Version stable, aprelic, connexion CFRS et autres corrections
This commit is contained in:
1 parent
865de8a976
commit
e9d0010451
62 files changed
+17483
-7256
No files matched your search
+425
@@ -0,0 +1,425 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
require_once __DIR__ . '/../includes/bootstrap_global.php';
|
||||
|
||||
if (session_status() !== PHP_SESSION_ACTIVE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
function h(?string $value): string
|
||||
{
|
||||
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
$flashSuccess = $_SESSION['aprelik_success'] ?? '';
|
||||
$flashErrors = $_SESSION['aprelik_errors'] ?? [];
|
||||
$old = $_SESSION['aprelik_old'] ?? [];
|
||||
|
||||
unset($_SESSION['aprelik_success'], $_SESSION['aprelik_errors'], $_SESSION['aprelik_old']);
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Запись в Апрелик 2026-2027 — USLUGI.FR</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--navy: #1a2744;
|
||||
--blue: #6b7fd4;
|
||||
--blue-light: #eef0fb;
|
||||
--red: #e03e3e;
|
||||
--white: #ffffff;
|
||||
--gray-bg: #f4f6fb;
|
||||
--muted: #6b7a90;
|
||||
--border: #dde2f0;
|
||||
--success-bg: #eef9f0;
|
||||
--success-border: #9dd8a6;
|
||||
--success-text: #216a2f;
|
||||
--error-bg: #fff1f1;
|
||||
--error-border: #efb2b2;
|
||||
--error-text: #a12c2c;
|
||||
}
|
||||
|
||||
body { font-family: 'Montserrat', sans-serif; background: var(--white); color: var(--navy); font-size: 15px; line-height: 1.6; }
|
||||
|
||||
.hero { padding: 48px 24px 40px; text-align: center; max-width: 640px; margin: 0 auto; }
|
||||
|
||||
.school-block {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.school-logo {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
object-fit: cover;
|
||||
border: 2px solid var(--border);
|
||||
}
|
||||
|
||||
.school-info { text-align: left; }
|
||||
.school-info h2 { font-size: 17px; font-weight: 800; color: var(--navy); line-height: 1.3; }
|
||||
.school-info p { font-size: 12px; color: var(--muted); margin-top: 2px; }
|
||||
|
||||
.tag { display: inline-block; border: 1.5px solid var(--border); border-radius: 100px; padding: 5px 16px; font-size: 11px; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted); margin-bottom: 16px; }
|
||||
|
||||
.hero h1 { font-size: clamp(24px, 6vw, 32px); font-weight: 800; color: var(--navy); line-height: 1.2; margin-bottom: 12px; }
|
||||
.hero p { font-size: 15px; color: var(--muted); max-width: 400px; margin: 0 auto; }
|
||||
|
||||
.form-section { background: var(--gray-bg); padding: 40px 24px; }
|
||||
.form-inner { max-width: 640px; margin: 0 auto; }
|
||||
|
||||
.notice {
|
||||
border-radius: 16px;
|
||||
padding: 14px 16px;
|
||||
margin-bottom: 16px;
|
||||
border: 1px solid transparent;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.notice-success {
|
||||
background: var(--success-bg);
|
||||
border-color: var(--success-border);
|
||||
color: var(--success-text);
|
||||
}
|
||||
|
||||
.notice-error {
|
||||
background: var(--error-bg);
|
||||
border-color: var(--error-border);
|
||||
color: var(--error-text);
|
||||
}
|
||||
|
||||
.notice ul {
|
||||
margin: 8px 0 0 18px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.form-block { background: var(--white); border-radius: 20px; padding: 28px 24px; margin-bottom: 16px; }
|
||||
.form-block-title { font-size: 13px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); margin-bottom: 20px; padding-bottom: 12px; border-bottom: 1px solid var(--border); }
|
||||
|
||||
.form-row { margin-bottom: 16px; }
|
||||
.form-row:last-child { margin-bottom: 0; }
|
||||
|
||||
.form-row-2 { display: flex; gap: 12px; }
|
||||
.form-row-2 .form-row { flex: 1; margin-bottom: 0; }
|
||||
|
||||
label { display: block; font-size: 11px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--navy); margin-bottom: 6px; }
|
||||
label .req { color: var(--red); margin-left: 2px; }
|
||||
|
||||
input[type="text"],
|
||||
input[type="email"],
|
||||
input[type="date"],
|
||||
input[type="tel"],
|
||||
select,
|
||||
textarea {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
border: 1.5px solid var(--border);
|
||||
border-radius: 12px;
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
font-size: 14px;
|
||||
color: var(--navy);
|
||||
background: var(--white);
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
input:focus, select:focus, textarea:focus { border-color: var(--blue); }
|
||||
|
||||
select {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%236b7a90' stroke-width='1.5' fill='none'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 16px center;
|
||||
padding-right: 40px;
|
||||
}
|
||||
|
||||
textarea { resize: vertical; min-height: 80px; }
|
||||
|
||||
.checkbox-row { display: flex; align-items: flex-start; gap: 12px; margin-bottom: 14px; }
|
||||
.checkbox-row:last-child { margin-bottom: 0; }
|
||||
|
||||
.checkbox-row input[type="checkbox"] {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
min-width: 20px;
|
||||
border: 1.5px solid var(--border);
|
||||
border-radius: 6px;
|
||||
margin-top: 2px;
|
||||
cursor: pointer;
|
||||
accent-color: var(--navy);
|
||||
}
|
||||
|
||||
.checkbox-row label {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--navy);
|
||||
text-transform: none;
|
||||
letter-spacing: 0;
|
||||
cursor: pointer;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.checkbox-row label .req { color: var(--red); }
|
||||
|
||||
.submit-section { padding: 32px 24px 48px; max-width: 640px; margin: 0 auto; text-align: center; }
|
||||
|
||||
.btn {
|
||||
display: block;
|
||||
background: var(--red);
|
||||
color: var(--white);
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
padding: 16px 36px;
|
||||
border-radius: 100px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
margin: 0 auto 16px;
|
||||
}
|
||||
|
||||
.submit-note { font-size: 12px; color: var(--muted); line-height: 1.5; }
|
||||
|
||||
.footer { text-align: center; padding: 24px; border-top: 1px solid var(--border); }
|
||||
.footer img { height: 32px; width: auto; }
|
||||
|
||||
.hidden-field {
|
||||
position: absolute !important;
|
||||
left: -9999px !important;
|
||||
width: 1px !important;
|
||||
height: 1px !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.hero { padding: 36px 18px 30px; }
|
||||
.school-block { flex-direction: column; }
|
||||
.school-info { text-align: center; }
|
||||
.form-section { padding: 28px 16px; }
|
||||
.form-block { padding: 22px 16px; }
|
||||
.form-row-2 { flex-direction: column; gap: 16px; }
|
||||
.submit-section { padding: 24px 16px 40px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="hero">
|
||||
<div class="school-block">
|
||||
<img class="school-logo" src="/public/uploads/img/aprelik.jpeg" alt="Апрелик">
|
||||
<div class="school-info">
|
||||
<h2>Студия-мастерская<br>«Апрелик»</h2>
|
||||
<p>Русский язык и театр в Париже</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tag">Набор открыт</div>
|
||||
<h1>Запись на 2026-2027<br>учебный год</h1>
|
||||
<br>
|
||||
<p>Заполните форму — заявка поступит напрямую в школу.</p>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<div class="form-inner">
|
||||
|
||||
<?php if ($flashSuccess !== ''): ?>
|
||||
<div class="notice notice-success"><?php echo h($flashSuccess); ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (! empty($flashErrors)): ?>
|
||||
<div class="notice notice-error">
|
||||
<strong>Пожалуйста, проверьте форму:</strong>
|
||||
<ul>
|
||||
<?php foreach ($flashErrors as $error): ?>
|
||||
<li><?php echo h((string) $error); ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form action="/aprelik_submit.php" method="post" accept-charset="UTF-8" novalidate>
|
||||
<div class="hidden-field" aria-hidden="true">
|
||||
<label for="website">Website</label>
|
||||
<input type="text" name="website" id="website" value="">
|
||||
</div>
|
||||
|
||||
<!-- Блок 1 — Ребёнок -->
|
||||
<div class="form-block">
|
||||
<div class="form-block-title">Данные ребёнка</div>
|
||||
|
||||
<div class="form-row-2">
|
||||
<div class="form-row">
|
||||
<label for="child_last_name">Фамилия ребёнка <span class="req">*</span></label>
|
||||
<input type="text" id="child_last_name" name="child_last_name" placeholder="Иванов" value="<?php echo h($old['child_last_name'] ?? ''); ?>">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="child_first_name">Имя ребёнка <span class="req">*</span></label>
|
||||
<input type="text" id="child_first_name" name="child_first_name" placeholder="Иван" value="<?php echo h($old['child_first_name'] ?? ''); ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="child_birthdate">Дата рождения <span class="req">*</span></label>
|
||||
<input type="date" id="child_birthdate" name="child_birthdate" value="<?php echo h($old['child_birthdate'] ?? ''); ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Блок 2 — Родители -->
|
||||
<div class="form-block">
|
||||
<div class="form-block-title">Данные родителей</div>
|
||||
|
||||
<div class="form-row-2">
|
||||
<div class="form-row">
|
||||
<label for="parent1_last_name">Фамилия родителя 1 <span class="req">*</span></label>
|
||||
<input type="text" id="parent1_last_name" name="parent1_last_name" placeholder="Иванова" value="<?php echo h($old['parent1_last_name'] ?? ''); ?>">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="parent1_first_name">Имя родителя 1 <span class="req">*</span></label>
|
||||
<input type="text" id="parent1_first_name" name="parent1_first_name" placeholder="Мария" value="<?php echo h($old['parent1_first_name'] ?? ''); ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="parent1_language">Родной язык родителя 1 <span class="req">*</span></label>
|
||||
<input type="text" id="parent1_language" name="parent1_language" placeholder="Русский" value="<?php echo h($old['parent1_language'] ?? ''); ?>">
|
||||
</div>
|
||||
|
||||
<div class="form-row-2">
|
||||
<div class="form-row">
|
||||
<label for="parent2_last_name">Фамилия родителя 2</label>
|
||||
<input type="text" id="parent2_last_name" name="parent2_last_name" placeholder="Иванов" value="<?php echo h($old['parent2_last_name'] ?? ''); ?>">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="parent2_first_name">Имя родителя 2</label>
|
||||
<input type="text" id="parent2_first_name" name="parent2_first_name" placeholder="Пётр" value="<?php echo h($old['parent2_first_name'] ?? ''); ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="parent2_language">Родной язык родителя 2</label>
|
||||
<input type="text" id="parent2_language" name="parent2_language" placeholder="Французский" value="<?php echo h($old['parent2_language'] ?? ''); ?>">
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="street_address">Адрес <span class="req">*</span></label>
|
||||
<input type="text" id="street_address" name="street_address" placeholder="10 rue de la Paix" value="<?php echo h($old['street_address'] ?? ''); ?>">
|
||||
</div>
|
||||
|
||||
<div class="form-row-2">
|
||||
<div class="form-row">
|
||||
<label for="postal_code">Индекс <span class="req">*</span></label>
|
||||
<input type="text" id="postal_code" name="postal_code" placeholder="75001" value="<?php echo h($old['postal_code'] ?? ''); ?>">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="city">Город <span class="req">*</span></label>
|
||||
<input type="text" id="city" name="city" placeholder="Paris" value="<?php echo h($old['city'] ?? ''); ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row-2">
|
||||
<div class="form-row">
|
||||
<label for="parent1_phone">Телефон родителя 1 <span class="req">*</span></label>
|
||||
<input type="tel" id="parent1_phone" name="parent1_phone" placeholder="+33 6 00 00 00 00" value="<?php echo h($old['parent1_phone'] ?? ''); ?>">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="parent2_phone">Телефон родителя 2</label>
|
||||
<input type="tel" id="parent2_phone" name="parent2_phone" placeholder="+33 6 00 00 00 00" value="<?php echo h($old['parent2_phone'] ?? ''); ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-row">
|
||||
<label for="email">Email <span class="req">*</span></label>
|
||||
<input type="email" id="email" name="email" placeholder="votre@email.com" value="<?php echo h($old['email'] ?? ''); ?>">
|
||||
</div>
|
||||
|
||||
<div class="form-block">
|
||||
<div class="form-block-title">Выбор расписания</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="school_day_address">Адрес занятий <span class="req">*</span></label>
|
||||
<select id="school_day_address" name="school_day_address">
|
||||
<option value="">Выберите адрес</option>
|
||||
<?php
|
||||
$addressOptions = [
|
||||
'Среда — 121 Av. Achille Peretti, 92200 Neuilly-sur-Seine',
|
||||
'Суббота — 10, rue du Cygne, Paris 75001',
|
||||
'Суббота — 60, rue de Greneta, Paris 75002',
|
||||
'Суббота — 2, avenue Rapp, Paris 75007',
|
||||
'Воскресенье — 2, avenue Rapp, Paris 75007',
|
||||
];
|
||||
$selectedAddress = (string) ($old['school_day_address'] ?? '');
|
||||
foreach ($addressOptions as $option):
|
||||
?>
|
||||
<option value="<?php echo h($option); ?>" <?php echo $selectedAddress === $option ? 'selected' : ''; ?>>
|
||||
<?php echo h($option); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="age_group">Возрастная группа <span class="req">*</span></label>
|
||||
<select id="age_group" name="age_group">
|
||||
<option value="">Выберите группу</option>
|
||||
<?php
|
||||
$ageOptions = [
|
||||
'3–4 года',
|
||||
'3-7 лет',
|
||||
'5-7 лет',
|
||||
'4-6 лет',
|
||||
'7-12 лет',
|
||||
'8-13 лет',
|
||||
'11-13 лет',
|
||||
'13-14 лет',
|
||||
];
|
||||
$selectedAge = (string) ($old['age_group'] ?? '');
|
||||
foreach ($ageOptions as $option):
|
||||
?>
|
||||
<option value="<?php echo h($option); ?>" <?php echo $selectedAge === $option ? 'selected' : ''; ?>>
|
||||
<?php echo h($option); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="notes">Комментарий</label>
|
||||
<textarea id="notes" name="notes" placeholder="Если хотите, можете уточнить удобное время, возраст ребёнка или другие детали."><?php echo h($old['notes'] ?? ''); ?></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="checkbox-row">
|
||||
<input type="checkbox" id="cb-diffusion" name="consent_email_diffusion" value="1" <?php echo ! empty($old['consent_email_diffusion']) ? 'checked' : ''; ?>>
|
||||
<label for="cb-diffusion">Согласен(а) на сохранение email и его использование для последующей информационной рассылки школы и платформы uslugi.fr <span class="req">*</span></label>
|
||||
</div>
|
||||
|
||||
<div class="checkbox-row">
|
||||
<input type="checkbox" id="cb-rgpd" name="consent_rgpd" value="1" <?php echo ! empty($old['consent_rgpd']) ? 'checked' : ''; ?>>
|
||||
<label for="cb-rgpd">Согласен(а) на обработку персональных данных в соответствии с законодательством Франции (RGPD) <span class="req">*</span></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="submit-section">
|
||||
<button class="btn" type="submit">Отправить заявку</button>
|
||||
<p class="submit-note">Форма используется для сохранения ваших контактных данных и последующей информационной рассылки с платформы uslugi.fr</p>
|
||||
|
||||
<div style="margin-top: 24px;">
|
||||
<a href="https://uslugi.fr" target="_blank" rel="noopener noreferrer">
|
||||
<img src="/public/uploads/img/logo.png" alt="USLUGI.FR" style="height: 42px; width: auto;">
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,2 @@
|
||||
<?php
|
||||
require __DIR__ . '/../aprelik.php';
|
||||
@@ -0,0 +1,257 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
require_once __DIR__ . '/../includes/bootstrap_global.php';
|
||||
|
||||
if (session_status() !== PHP_SESSION_ACTIVE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
function aprelik_redirect(string $path): never
|
||||
{
|
||||
header('Location: ' . $path);
|
||||
exit;
|
||||
}
|
||||
|
||||
function aprelik_clean(?string $value, int $maxLength = 0): string
|
||||
{
|
||||
$value = trim((string) $value);
|
||||
$value = preg_replace('/\s+/u', ' ', $value) ?? '';
|
||||
|
||||
if ($maxLength > 0 && mb_strlen($value) > $maxLength) {
|
||||
$value = mb_substr($value, 0, $maxLength);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
function aprelik_get_pdo(): PDO
|
||||
{
|
||||
foreach (['pdo', 'db', 'database', 'conn'] as $name) {
|
||||
if (isset($GLOBALS[$name]) && $GLOBALS[$name] instanceof PDO) {
|
||||
return $GLOBALS[$name];
|
||||
}
|
||||
}
|
||||
|
||||
throw new RuntimeException('PDO connection not found in bootstrap_global.php');
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
aprelik_redirect('/aprelik.php');
|
||||
}
|
||||
|
||||
if (! empty($_POST['website'] ?? '')) {
|
||||
$_SESSION['aprelik_success'] = 'Спасибо. Форма отправлена.';
|
||||
aprelik_redirect('/aprelik.php');
|
||||
}
|
||||
|
||||
$data = [
|
||||
'child_last_name' => aprelik_clean($_POST['child_last_name'] ?? '', 120),
|
||||
'child_first_name' => aprelik_clean($_POST['child_first_name'] ?? '', 120),
|
||||
'child_birthdate' => aprelik_clean($_POST['child_birthdate'] ?? '', 10),
|
||||
|
||||
'parent1_last_name' => aprelik_clean($_POST['parent1_last_name'] ?? '', 120),
|
||||
'parent1_first_name' => aprelik_clean($_POST['parent1_first_name'] ?? '', 120),
|
||||
'parent1_language' => aprelik_clean($_POST['parent1_language'] ?? '', 120),
|
||||
|
||||
'parent2_last_name' => aprelik_clean($_POST['parent2_last_name'] ?? '', 120),
|
||||
'parent2_first_name' => aprelik_clean($_POST['parent2_first_name'] ?? '', 120),
|
||||
'parent2_language' => aprelik_clean($_POST['parent2_language'] ?? '', 120),
|
||||
|
||||
'street_address' => aprelik_clean($_POST['street_address'] ?? '', 255),
|
||||
'postal_code' => aprelik_clean($_POST['postal_code'] ?? '', 30),
|
||||
'city' => aprelik_clean($_POST['city'] ?? '', 120),
|
||||
|
||||
'parent1_phone' => aprelik_clean($_POST['parent1_phone'] ?? '', 40),
|
||||
'parent2_phone' => aprelik_clean($_POST['parent2_phone'] ?? '', 40),
|
||||
'email' => aprelik_clean($_POST['email'] ?? '', 190),
|
||||
|
||||
'consent_rgpd' => ! empty($_POST['consent_rgpd']) ? 1 : 0,
|
||||
'consent_email_diffusion' => ! empty($_POST['consent_email_diffusion']) ? 1 : 0,
|
||||
|
||||
'school_day_address' => aprelik_clean($_POST['school_day_address'] ?? '', 255),
|
||||
'age_group' => aprelik_clean($_POST['age_group'] ?? '', 80),
|
||||
'notes' => aprelik_clean($_POST['notes'] ?? '', 5000),
|
||||
];
|
||||
|
||||
$errors = [];
|
||||
|
||||
if ($data['child_last_name'] === '') {
|
||||
$errors[] = 'Укажите фамилию ребёнка.';
|
||||
}
|
||||
|
||||
if ($data['child_first_name'] === '') {
|
||||
$errors[] = 'Укажите имя ребёнка.';
|
||||
}
|
||||
|
||||
if ($data['child_birthdate'] === '') {
|
||||
$errors[] = 'Укажите дату рождения ребёнка.';
|
||||
} else {
|
||||
$dt = DateTime::createFromFormat('Y-m-d', $data['child_birthdate']);
|
||||
if (! $dt || $dt->format('Y-m-d') !== $data['child_birthdate']) {
|
||||
$errors[] = 'Некорректная дата рождения.';
|
||||
}
|
||||
}
|
||||
|
||||
if ($data['parent1_last_name'] === '') {
|
||||
$errors[] = 'Укажите фамилию родителя 1.';
|
||||
}
|
||||
|
||||
if ($data['parent1_first_name'] === '') {
|
||||
$errors[] = 'Укажите имя родителя 1.';
|
||||
}
|
||||
|
||||
if ($data['parent1_language'] === '') {
|
||||
$errors[] = 'Укажите язык родителя 1.';
|
||||
}
|
||||
|
||||
if ($data['street_address'] === '') {
|
||||
$errors[] = 'Укажите адрес.';
|
||||
}
|
||||
|
||||
if ($data['postal_code'] === '') {
|
||||
$errors[] = 'Укажите индекс.';
|
||||
}
|
||||
|
||||
if ($data['city'] === '') {
|
||||
$errors[] = 'Укажите город.';
|
||||
}
|
||||
|
||||
if ($data['parent1_phone'] === '') {
|
||||
$errors[] = 'Укажите телефон родителя 1.';
|
||||
}
|
||||
|
||||
if ($data['email'] === '' || filter_var($data['email'], FILTER_VALIDATE_EMAIL) === false) {
|
||||
$errors[] = 'Укажите корректный email.';
|
||||
}
|
||||
|
||||
if ($data['school_day_address'] === '') {
|
||||
$errors[] = 'Выберите адрес занятий.';
|
||||
}
|
||||
|
||||
if ($data['age_group'] === '') {
|
||||
$errors[] = 'Выберите возрастную группу.';
|
||||
}
|
||||
|
||||
if ($data['consent_email_diffusion'] !== 1) {
|
||||
$errors[] = 'Нужно согласиться на информационную рассылку.';
|
||||
}
|
||||
|
||||
if ($data['consent_rgpd'] !== 1) {
|
||||
$errors[] = 'Нужно согласиться на обработку персональных данных.';
|
||||
}
|
||||
|
||||
if ($errors !== []) {
|
||||
$_SESSION['aprelik_errors'] = $errors;
|
||||
$_SESSION['aprelik_old'] = $data;
|
||||
aprelik_redirect('/aprelik.php');
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo = aprelik_get_pdo();
|
||||
|
||||
$sql = "
|
||||
INSERT INTO aprelik (
|
||||
child_last_name,
|
||||
child_first_name,
|
||||
child_birthdate,
|
||||
parent1_last_name,
|
||||
parent1_first_name,
|
||||
parent1_language,
|
||||
parent2_last_name,
|
||||
parent2_first_name,
|
||||
parent2_language,
|
||||
street_address,
|
||||
postal_code,
|
||||
city,
|
||||
parent1_phone,
|
||||
parent2_phone,
|
||||
email,
|
||||
consent_rgpd,
|
||||
consent_email_diffusion,
|
||||
ip_address,
|
||||
user_agent,
|
||||
school_day_address,
|
||||
age_group,
|
||||
notes
|
||||
) VALUES (
|
||||
:child_last_name,
|
||||
:child_first_name,
|
||||
:child_birthdate,
|
||||
:parent1_last_name,
|
||||
:parent1_first_name,
|
||||
:parent1_language,
|
||||
:parent2_last_name,
|
||||
:parent2_first_name,
|
||||
:parent2_language,
|
||||
:street_address,
|
||||
:postal_code,
|
||||
:city,
|
||||
:parent1_phone,
|
||||
:parent2_phone,
|
||||
:email,
|
||||
:consent_rgpd,
|
||||
:consent_email_diffusion,
|
||||
:ip_address,
|
||||
:user_agent,
|
||||
:school_day_address,
|
||||
:age_group,
|
||||
:notes
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
child_last_name = VALUES(child_last_name),
|
||||
child_first_name = VALUES(child_first_name),
|
||||
child_birthdate = VALUES(child_birthdate),
|
||||
parent1_last_name = VALUES(parent1_last_name),
|
||||
parent1_first_name = VALUES(parent1_first_name),
|
||||
parent1_language = VALUES(parent1_language),
|
||||
parent2_last_name = VALUES(parent2_last_name),
|
||||
parent2_first_name = VALUES(parent2_first_name),
|
||||
parent2_language = VALUES(parent2_language),
|
||||
street_address = VALUES(street_address),
|
||||
postal_code = VALUES(postal_code),
|
||||
city = VALUES(city),
|
||||
parent1_phone = VALUES(parent1_phone),
|
||||
parent2_phone = VALUES(parent2_phone),
|
||||
consent_rgpd = VALUES(consent_rgpd),
|
||||
consent_email_diffusion = VALUES(consent_email_diffusion),
|
||||
ip_address = VALUES(ip_address),
|
||||
user_agent = VALUES(user_agent),
|
||||
school_day_address = VALUES(school_day_address),
|
||||
age_group = VALUES(age_group),
|
||||
notes = VALUES(notes),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
";
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute([
|
||||
':child_last_name' => $data['child_last_name'],
|
||||
':child_first_name' => $data['child_first_name'],
|
||||
':child_birthdate' => $data['child_birthdate'],
|
||||
':parent1_last_name' => $data['parent1_last_name'],
|
||||
':parent1_first_name' => $data['parent1_first_name'],
|
||||
':parent1_language' => $data['parent1_language'],
|
||||
':parent2_last_name' => $data['parent2_last_name'] !== '' ? $data['parent2_last_name'] : null,
|
||||
':parent2_first_name' => $data['parent2_first_name'] !== '' ? $data['parent2_first_name'] : null,
|
||||
':parent2_language' => $data['parent2_language'] !== '' ? $data['parent2_language'] : null,
|
||||
':street_address' => $data['street_address'],
|
||||
':postal_code' => $data['postal_code'],
|
||||
':city' => $data['city'],
|
||||
':parent1_phone' => $data['parent1_phone'],
|
||||
':parent2_phone' => $data['parent2_phone'] !== '' ? $data['parent2_phone'] : null,
|
||||
':email' => mb_strtolower($data['email']),
|
||||
':consent_rgpd' => $data['consent_rgpd'],
|
||||
':consent_email_diffusion' => $data['consent_email_diffusion'],
|
||||
':ip_address' => substr((string) ($_SERVER['REMOTE_ADDR'] ?? ''), 0, 45) ?: null,
|
||||
':user_agent' => substr((string) ($_SERVER['HTTP_USER_AGENT'] ?? ''), 0, 255) ?: null,
|
||||
':school_day_address' => $data['school_day_address'],
|
||||
':age_group' => $data['age_group'],
|
||||
':notes' => $data['notes'] !== '' ? $data['notes'] : null,
|
||||
]);
|
||||
|
||||
$_SESSION['aprelik_success'] = 'Спасибо. Ваша заявка сохранена.';
|
||||
aprelik_redirect('/aprelik.php');
|
||||
} catch (Throwable $e) {
|
||||
$_SESSION['aprelik_errors'] = ['Произошла ошибка при сохранении формы.'];
|
||||
$_SESSION['aprelik_old'] = $data;
|
||||
aprelik_redirect('/aprelik.php');
|
||||
}
|
||||
+187
-158
@@ -1,62 +1,99 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/public/includes/init_auth.php';
|
||||
|
||||
$erreur = '';
|
||||
$email = '';
|
||||
$mot_de_passe = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
|
||||
$email = trim($_POST['email'] ?? '');
|
||||
$mot_de_passe = $_POST['mot_de_passe'] ?? '';
|
||||
|
||||
if ($email === '' || $mot_de_passe === '') {
|
||||
$erreur = $translations['erreur_champs_vides'];
|
||||
// 🔒 --- Protection brute force ---
|
||||
$_SESSION['login_tries'] = (int) ($_SESSION['login_tries'] ?? 0);
|
||||
$blockedUntil = (int) ($_SESSION['login_blocked_until'] ?? 0);
|
||||
|
||||
if ($blockedUntil > time()) {
|
||||
$remaining = $blockedUntil - time();
|
||||
$erreur = sprintf(
|
||||
$translations['trop_de_tentatives_connexion'] ?? 'Trop de tentatives. Réessayez dans %d secondes.',
|
||||
$remaining
|
||||
);
|
||||
} else {
|
||||
// 1) On vérifie l’utilisateur
|
||||
$stmt = $pdo->prepare("SELECT * FROM compte WHERE email = :email LIMIT 1");
|
||||
$stmt->execute(['email' => $email]);
|
||||
$compte = $stmt->fetch();
|
||||
|
||||
if ($compte && password_verify($mot_de_passe, $compte['mot_de_passe'])) {
|
||||
if (empty($compte['email_valide'])) {
|
||||
$_SESSION['pending_validation_resend'] = [
|
||||
'compte_id' => (int) $compte['id'],
|
||||
'email' => (string) $compte['email'],
|
||||
'authorized' => true,
|
||||
'expires_at' => time() + 900, // 15 min
|
||||
];
|
||||
|
||||
$erreur = $translations['tvalidat_erreur_email_non_valide'] ?? $translations['erreur_email_non_valide'] ?? 'Veuillez valider votre adresse email avant de vous connecter.';
|
||||
} else {
|
||||
session_regenerate_id(true); // ✅ anti session fixation
|
||||
// 2) Connexion OK → on initialise la session
|
||||
$_SESSION['compte_id'] = $compte['id'];
|
||||
$_SESSION['role'] = strtolower(trim($compte['role'] ?? ''));
|
||||
$_SESSION['reference_id'] = $compte['reference_id'];
|
||||
$_SESSION['email'] = $compte['email'];
|
||||
$_SESSION['last_activity'] = time();
|
||||
|
||||
// 3) On gère la redirection prioritaire vers la page demandée
|
||||
$next = $_POST['next'] ?? $_GET['next'] ?? ($_SESSION['redirect_to'] ?? null);
|
||||
$next = next_safe($next);
|
||||
if ($next) {
|
||||
unset($_SESSION['redirect_to']);
|
||||
header('Location: ' . $next);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 4) Sinon, fallback par rôle
|
||||
if ($_SESSION['role'] === 'client') {
|
||||
header('Location: /public/client/mes_rendez_vous.php');exit;
|
||||
} elseif ($_SESSION['role'] === 'admin') {
|
||||
header('Location: /admin/index.php');exit;
|
||||
} elseif ($_SESSION['role'] === 'secretaire') {
|
||||
header('Location: /public/cabinet/dashboard.php');exit;
|
||||
} else {
|
||||
header('Location: /public/praticien/agenda.php');exit;
|
||||
}
|
||||
}
|
||||
if ($email === '' || $mot_de_passe === '') {
|
||||
$erreur = $translations['erreur_champs_vides'];
|
||||
} else {
|
||||
$erreur = $translations['erreur_identifiants'];
|
||||
|
||||
$stmt = $pdo->prepare("SELECT * FROM compte WHERE email = :email LIMIT 1");
|
||||
$stmt->execute(['email' => $email]);
|
||||
$compte = $stmt->fetch();
|
||||
|
||||
if ($compte && password_verify($mot_de_passe, $compte['mot_de_passe'])) {
|
||||
|
||||
// ✅ reset brute force
|
||||
$_SESSION['login_tries'] = 0;
|
||||
unset($_SESSION['login_blocked_until']);
|
||||
|
||||
if (empty($compte['email_valide'])) {
|
||||
|
||||
$_SESSION['pending_validation_resend'] = [
|
||||
'compte_id' => (int) $compte['id'],
|
||||
'email' => (string) $compte['email'],
|
||||
'authorized' => true,
|
||||
'expires_at' => time() + 900,
|
||||
];
|
||||
|
||||
$erreur = $translations['tvalidat_erreur_email_non_valide'] ?? $translations['erreur_email_non_valide'] ?? 'Veuillez valider votre adresse email.';
|
||||
|
||||
} else {
|
||||
|
||||
session_regenerate_id(true);
|
||||
|
||||
$_SESSION['compte_id'] = $compte['id'];
|
||||
$_SESSION['role'] = strtolower(trim($compte['role'] ?? ''));
|
||||
$_SESSION['reference_id'] = $compte['reference_id'];
|
||||
$_SESSION['email'] = $compte['email'];
|
||||
$_SESSION['last_activity'] = time();
|
||||
|
||||
// 🔁 redirection next
|
||||
$next = $_POST['next'] ?? $_GET['next'] ?? ($_SESSION['redirect_to'] ?? null);
|
||||
$next = next_safe($next);
|
||||
|
||||
if ($next) {
|
||||
unset($_SESSION['redirect_to']);
|
||||
header('Location: ' . $next);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 🎯 fallback rôle
|
||||
if ($_SESSION['role'] === 'client') {
|
||||
header('Location: /public/client/mes_rendez_vous.php');exit;
|
||||
} elseif ($_SESSION['role'] === 'admin') {
|
||||
header('Location: /admin/index.php');exit;
|
||||
} elseif ($_SESSION['role'] === 'secretaire') {
|
||||
header('Location: /public/cabinet/dashboard.php');exit;
|
||||
} else {
|
||||
header('Location: /public/praticien/agenda.php');exit;
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
// ❌ Mauvais login
|
||||
$_SESSION['login_tries']++;
|
||||
|
||||
// ⏱️ Blocage après 5 tentatives
|
||||
if ($_SESSION['login_tries'] >= 3) {
|
||||
$_SESSION['login_blocked_until'] = time() + 600; // 10 min
|
||||
}
|
||||
|
||||
// 🐢 ralentissement progressif
|
||||
sleep(min($_SESSION['login_tries'], 3));
|
||||
|
||||
$erreur = $translations['erreur_identifiants'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,17 +103,13 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title><?php echo htmlspecialchars($translations['seo_connexion_title'], ENT_QUOTES, 'UTF-8') ?></title>
|
||||
<meta name="description" content="<?php echo htmlspecialchars($translations['seo_connexion_desc'], ENT_QUOTES, 'UTF-8') ?>">
|
||||
<meta name="keywords" content="<?php echo htmlspecialchars($translations['seo_connexion_keywords'], ENT_QUOTES, 'UTF-8') ?>">
|
||||
<meta name="author" content="Uslugi.fr">
|
||||
|
||||
|
||||
<!-- Recommandé pour une page de login -->
|
||||
<meta name="robots" content="noindex, nofollow, noarchive, nosnippet">
|
||||
<meta name="description" content="<?php echo htmlspecialchars($translations['seo_connexion_desc'], ENT_QUOTES, 'UTF-8') ?>">
|
||||
<meta name="keywords" content="<?php echo htmlspecialchars($translations['seo_connexion_keywords'], ENT_QUOTES, 'UTF-8') ?>">
|
||||
<meta name="author" content="Uslugi.fr">
|
||||
<meta name="robots" content="noindex, nofollow, noarchive, nosnippet">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
|
||||
<link rel="stylesheet" href="<?php echo asset('/includes/site_style.css') ?>">
|
||||
<link rel="stylesheet" href="<?php echo asset('/includes/site_style.css') ?>">
|
||||
<link rel="stylesheet" href="<?php echo asset('/includes/custom_style.css') ?>">
|
||||
<link rel="stylesheet" href="<?php echo asset('/includes/mobil_style.css') ?>">
|
||||
</head>
|
||||
@@ -84,130 +117,126 @@
|
||||
<header class="site-header">
|
||||
<div class="container">
|
||||
<div class="header-left">
|
||||
<div class="brand">
|
||||
<a href="/index.php"><img src="/public/uploads/img/logo_u.png"
|
||||
alt="uslugi.fr"
|
||||
class="logo-mobile"></a>
|
||||
|
||||
<a href="/index.php"><img src="/public/uploads/img/logo.png"
|
||||
alt="uslugi.fr"
|
||||
class="logo-desktop"></a>
|
||||
</div>
|
||||
<div class="lang-switch">
|
||||
<div class="brand">
|
||||
<a href="/index.php"><img src="/public/uploads/img/logo_u.png" alt="uslugi.fr" class="logo-mobile"></a>
|
||||
<a href="/index.php"><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>
|
||||
</div>
|
||||
|
||||
<nav class="header-actions">
|
||||
<div class="dropdown">
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-logo rounded-circle d-inline-flex align-items-center justify-content-center question-btn"
|
||||
type="button" data-bs-toggle="dropdown" data-bs-auto-close="true"
|
||||
aria-expanded="false" aria-label="Помощь">?</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end shadow help-menu menu-panel">
|
||||
<li><a href="/how-it-work.php"><?php echo $translations['kak_rabotaet'] ?></a></a></li>
|
||||
<li><a href="/about.php"><?php echo $translations['pro_nas'] ?></a></li>
|
||||
<li><a href="/faq.php"><?php echo $translations['faq'] ?></a></li>
|
||||
<li><a href="/contact.php"><?php echo $translations['nam_pisat'] ?></a></li>
|
||||
<li><a href="/how-it-work.php"><?php echo $translations['kak_rabotaet'] ?></a></li>
|
||||
<li><a href="/about.php"><?php echo $translations['pro_nas'] ?></a></li>
|
||||
<li><a href="/faq.php"><?php echo $translations['faq'] ?></a></li>
|
||||
<li><a href="/contact.php"><?php echo $translations['nam_pisat'] ?></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="main">
|
||||
<div class="main">
|
||||
<div class="card shadow-sm p-4" style="min-width:380px; max-width:520px; margin:40px auto;">
|
||||
<h3 class="mb-4 text-center text-dark"><?php echo $translations['connexion_titre'] ?></h3>
|
||||
|
||||
<div class="card shadow-sm p-4" style="min-width:380px; max-width:520px; margin:40px auto;">
|
||||
<h3 class="mb-4 text-center text-dark"><?php echo $translations['connexion_titre'] ?></h3>
|
||||
<?php if ($erreur): ?>
|
||||
<?php
|
||||
$canResend = ! empty($_SESSION['pending_validation_resend']['authorized'])
|
||||
&& ! empty($_SESSION['pending_validation_resend']['expires_at'])
|
||||
&& (int) $_SESSION['pending_validation_resend']['expires_at'] >= time()
|
||||
&& ! empty($email)
|
||||
&& ! empty($_SESSION['pending_validation_resend']['email'])
|
||||
&& hash_equals(
|
||||
(string) $_SESSION['pending_validation_resend']['email'],
|
||||
(string) $email
|
||||
);
|
||||
?>
|
||||
<div class="alert alert-danger py-2 text-center" role="alert">
|
||||
<?php echo htmlspecialchars($erreur, ENT_QUOTES, 'UTF-8'); ?>
|
||||
|
||||
<?php if ($erreur): ?>
|
||||
<?php
|
||||
$canResend = ! empty($_SESSION['pending_validation_resend']['authorized'])
|
||||
&& ! empty($_SESSION['pending_validation_resend']['expires_at'])
|
||||
&& (int) $_SESSION['pending_validation_resend']['expires_at'] >= time()
|
||||
&& ! empty($email)
|
||||
&& ! empty($_SESSION['pending_validation_resend']['email'])
|
||||
&& hash_equals(
|
||||
(string) $_SESSION['pending_validation_resend']['email'],
|
||||
(string) $email
|
||||
);
|
||||
?>
|
||||
<div class="alert alert-danger py-2 text-center" role="alert">
|
||||
<?php echo htmlspecialchars($erreur, ENT_QUOTES, 'UTF-8'); ?>
|
||||
<?php if ($canResend): ?>
|
||||
<div class="mt-3">
|
||||
<form method="POST" action="/renvoyer_validat_mail.php" class="m-0">
|
||||
<input type="hidden" name="email"
|
||||
value="<?php echo htmlspecialchars($email, ENT_QUOTES, 'UTF-8'); ?>">
|
||||
<button type="submit" class="btn btn-light btn-sm">
|
||||
<?php echo htmlspecialchars($translations['tvalidat_btn_renvoyer'] ?? 'Renvoyer le mail de validation', ENT_QUOTES, 'UTF-8'); ?>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($canResend): ?>
|
||||
<div class="mt-3">
|
||||
<form method="POST" action="/renvoyer_validat_mail.php" class="m-0">
|
||||
<input type="hidden" name="email"
|
||||
value="<?php echo htmlspecialchars($email, ENT_QUOTES, 'UTF-8'); ?>">
|
||||
<button type="submit" class="btn btn-light btn-sm">
|
||||
<?php echo htmlspecialchars($translations['tvalidat_btn_renvoyer'] ?? 'Renvoyer le mail de validation', ENT_QUOTES, 'UTF-8'); ?>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<form method="POST">
|
||||
<div class="mb-3">
|
||||
<input type="email" name="email" class="form-control"
|
||||
placeholder="<?php echo $translations['email_placeholder'] ?>"
|
||||
value="<?php echo htmlspecialchars($email) ?>" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 position-relative">
|
||||
<input id="mot_de_passe" type="password" name="mot_de_passe" class="form-control"
|
||||
placeholder="<?php echo $translations['motdepasse_placeholder'] ?>"
|
||||
required autocomplete="current-password">
|
||||
<span class="toggle-password"
|
||||
id="eyePwd"
|
||||
onmouseenter="revealPwd('mot_de_passe')"
|
||||
onmouseleave="hidePwd('mot_de_passe')"
|
||||
onmousedown="revealPwd('mot_de_passe')"
|
||||
onmouseup="hidePwd('mot_de_passe')"
|
||||
ontouchstart="revealPwd('mot_de_passe')"
|
||||
ontouchend="hidePwd('mot_de_passe')"
|
||||
title="Наведите, чтобы показать">
|
||||
👁️
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="next"
|
||||
value="<?php echo htmlspecialchars($_GET['next'] ?? ($_SESSION['redirect_to'] ?? ''), ENT_QUOTES); ?>">
|
||||
|
||||
<button type="submit" class="btn btn-primary w-100"><?php echo $translations['btn_se_connecter'] ?></button>
|
||||
|
||||
<div class="text-center mt-3">
|
||||
<a href="mdp_oublie.php" class="me-2"><?php echo $translations['lien_mdp_oublie'] ?></a> |
|
||||
<a href="choix_inscr.php" class="ms-2"><?php echo $translations['lien_inscription'] ?></a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="POST">
|
||||
<div class="mb-3">
|
||||
<input type="email" name="email" class="form-control"
|
||||
placeholder="<?php echo $translations['email_placeholder'] ?>"
|
||||
value="<?php echo htmlspecialchars($email) ?>" required>
|
||||
</div>
|
||||
<div class="mb-3 position-relative">
|
||||
<input id="mot_de_passe" type="password" name="mot_de_passe" class="form-control"
|
||||
placeholder="<?php echo $translations['motdepasse_placeholder'] ?>"
|
||||
required autocomplete="current-password">
|
||||
<span class="toggle-password"
|
||||
id="eyePwd"
|
||||
onmouseenter="revealPwd('mot_de_passe')"
|
||||
onmouseleave="hidePwd('mot_de_passe')"
|
||||
onmousedown="revealPwd('mot_de_passe')"
|
||||
onmouseup="hidePwd('mot_de_passe')"
|
||||
ontouchstart="revealPwd('mot_de_passe')"
|
||||
ontouchend="hidePwd('mot_de_passe')"
|
||||
title="Наведите, чтобы показать">
|
||||
👁️
|
||||
</span>
|
||||
</div>
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p><?php echo $translations['copyr'] ?></p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- on propage le "next" -->
|
||||
<input type="hidden" name="next"
|
||||
value="<?php echo htmlspecialchars($_GET['next'] ?? ($_SESSION['redirect_to'] ?? ''), ENT_QUOTES) ?>">
|
||||
<script>
|
||||
function revealPwd(id){ var el = document.getElementById(id); if(el) el.type = 'text'; }
|
||||
function hidePwd(id){ var el = document.getElementById(id); if(el) el.type = 'password'; }
|
||||
|
||||
<button type="submit" class="btn btn-primary w-100"><?php echo $translations['btn_se_connecter'] ?></button>
|
||||
<div class="text-center mt-3">
|
||||
<a href="mdp_oublie.php" class="me-2"><?php echo $translations['lien_mdp_oublie'] ?></a> |
|
||||
<a href="choix_inscr.php" class="ms-2"><?php echo $translations['lien_inscription'] ?></a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
(function(){
|
||||
var eye = document.getElementById('eyePwd');
|
||||
var input = document.getElementById('mot_de_passe');
|
||||
if(!eye || !input) return;
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p><?php echo $translations['copyr'] ?></p>
|
||||
</div>
|
||||
</footer>
|
||||
<script>
|
||||
function revealPwd(id){ var el = document.getElementById(id); if(el) el.type = 'text'; }
|
||||
function hidePwd(id){ var el = document.getElementById(id); if(el) el.type = 'password'; }
|
||||
(function(){
|
||||
var eye = document.getElementById('eyePwd');
|
||||
var input = document.getElementById('mot_de_passe');
|
||||
if(!eye || !input) return;
|
||||
eye.addEventListener('touchstart', function(e){ e.preventDefault(); revealPwd('mot_de_passe'); }, {passive:false});
|
||||
eye.addEventListener('touchend', function(e){ e.preventDefault(); hidePwd('mot_de_passe'); }, {passive:false});
|
||||
eye.addEventListener('touchcancel',function(){ hidePwd('mot_de_passe'); });
|
||||
|
||||
// Mobile (iOS/Android) — empêche sélection/callout et révèle/masque
|
||||
eye.addEventListener('touchstart', function(e){ e.preventDefault(); revealPwd('mot_de_passe'); }, {passive:false});
|
||||
eye.addEventListener('touchend', function(e){ e.preventDefault(); hidePwd('mot_de_passe'); }, {passive:false});
|
||||
eye.addEventListener('touchcancel',function(){ hidePwd('mot_de_passe'); });
|
||||
eye.addEventListener('mousedown', function(e){ e.preventDefault(); revealPwd('mot_de_passe'); });
|
||||
eye.addEventListener('mouseup', function(e){ e.preventDefault(); hidePwd('mot_de_passe'); });
|
||||
eye.addEventListener('mouseleave',function(){ hidePwd('mot_de_passe'); });
|
||||
})();
|
||||
</script>
|
||||
|
||||
// Desktop / trackpad — empêche sélection au maintien
|
||||
eye.addEventListener('mousedown', function(e){ e.preventDefault(); revealPwd('mot_de_passe'); });
|
||||
eye.addEventListener('mouseup', function(e){ e.preventDefault(); hidePwd('mot_de_passe'); });
|
||||
eye.addEventListener('mouseleave',function(){ hidePwd('mot_de_passe'); });
|
||||
})();
|
||||
</script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,316 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../includes/bootstrap_global.php';
|
||||
require_once APP_HTDOCS . '/includes/avatar_initiales.php';
|
||||
$SPEC_CANON = require __DIR__ . '/includes/spec_map.php';
|
||||
|
||||
$canon = $SPEC_CANON[$lang] ?? [];
|
||||
|
||||
|
||||
|
||||
// ------- (ton code existant) -------
|
||||
$praticien_id = (int)($_GET['id'] ?? 0);
|
||||
if ($praticien_id <= 0) { echo "<div class='container py-5'>ID invalide.</div>"; exit; }
|
||||
|
||||
function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
|
||||
function percent_from_5($note){ $n = max(0,min(5,(float)$note)); return (int)round($n/5*100); }
|
||||
function pseudo_from($prenom,$nom){
|
||||
$p = trim((string)$prenom); $n = trim((string)$nom);
|
||||
$p = $p!=='' ? mb_convert_case($p, MB_CASE_TITLE, 'UTF-8') : 'Client';
|
||||
$i = $n!=='' ? mb_strtoupper(mb_substr($nom,0,1,'UTF-8'),'UTF-8') : '';
|
||||
return $i ? ($p.'.'.$i) : $p;
|
||||
}
|
||||
function video_thumb_info(?string $url): ?array {
|
||||
if (!$url || !trim($url)) return null; $u = trim($url);
|
||||
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"];
|
||||
}
|
||||
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"];
|
||||
}
|
||||
$path=parse_url($u,PHP_URL_PATH)??''; if(preg_match('~\.(mp4|webm|ogg)$~i',$path)){ return ['provider'=>'file','id'=>null,'thumb'=>null]; }
|
||||
if (stripos($u,'vimeo.com')!==false){ return ['provider'=>'vimeo','id'=>null,'thumb'=>null]; }
|
||||
return ['provider'=>'other','id'=>null,'thumb'=>null];
|
||||
}
|
||||
|
||||
// 1) Praticien
|
||||
$st = $pdo->prepare("SELECT id, titre, nom, prenom, telephone, adresse, ville, code_postal, specialite, presentation, photo, video, disponibilite
|
||||
FROM praticien WHERE id = ?");
|
||||
$st->execute([$praticien_id]);
|
||||
$p = $st->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$p) { echo "<div class='container py-5'>".$translations['praticien_introuvable']."</div>"; exit; }
|
||||
|
||||
$name = $name = trim($p['prenom'] . ' ' . $p['nom']);
|
||||
$photo = !empty($p['photo']) ? "/public/uploads/".ltrim($p['photo'],'/') : "/public/uploads/img/user_placeholder.png";
|
||||
|
||||
// 2) Vidéos additionnelles
|
||||
$vs = $pdo->prepare("SELECT id, url, title, ordre FROM praticien_video WHERE praticien_id=? ORDER BY ordre ASC, id DESC");
|
||||
$vs->execute([$praticien_id]);
|
||||
$videos = $vs->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// 3) Avis: moyenne + total + dernier
|
||||
$avg = $pdo->prepare("SELECT ROUND(AVG(note),1) AS m, COUNT(*) AS c FROM avis_praticien WHERE praticien_id=? AND statut='publie'");
|
||||
$avg->execute([$praticien_id]);
|
||||
$stats = $avg->fetch(PDO::FETCH_ASSOC) ?: ['m'=>null,'c'=>0];
|
||||
|
||||
$last = $pdo->prepare("SELECT a.note,a.titre,a.commentaire,a.visit_date,a.created_at,c.prenom,c.nom
|
||||
FROM avis_praticien a
|
||||
LEFT JOIN client c ON c.id=a.client_id
|
||||
WHERE a.praticien_id=? AND a.statut='publie'
|
||||
ORDER BY a.created_at DESC LIMIT 1");
|
||||
$last->execute([$praticien_id]);
|
||||
$lrow = $last->fetch(PDO::FETCH_ASSOC);
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title><?= $name . " - " . $p['specialite'] . " - " . htmlspecialchars($translations['seo_detail_public_title'], ENT_QUOTES, 'UTF-8')?></title>
|
||||
<meta name="description" content="<?= $name . " - " . $p['specialite'] . " - " . $p['presentation'] ?>">
|
||||
<meta name="keywords" content="<?= htmlspecialchars($translations['seo_detail_public_keywords'], ENT_QUOTES, 'UTF-8') ?>">
|
||||
<meta name="author" content="Uslugi.fr">
|
||||
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<link href="/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="<?= asset('/includes/site_style.css') ?>">
|
||||
<link rel="stylesheet" href="<?= asset('/includes/custom_style.css') ?>">
|
||||
<link rel="stylesheet" href="<?= asset('/includes/mobil_style.css') ?>">
|
||||
<?php include_once __DIR__ . '/includes/tracking.php'; ?>
|
||||
</head>
|
||||
<body class="layout">
|
||||
|
||||
<?php if ($logged):
|
||||
include_header_clean(APP_HTDOCS . '/public/includes/header.php');
|
||||
else: ?>
|
||||
<!-- Header public (Connexion + “?”) -->
|
||||
<header class="site-header">
|
||||
<div class="container">
|
||||
<div class="header-left">
|
||||
<div class="brand">
|
||||
<a href="/index.php"><img src="/public/uploads/img/logo_u.png"
|
||||
alt="uslugi.fr"
|
||||
class="logo-mobile"></a>
|
||||
|
||||
<a href="/index.php"><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>
|
||||
<nav class="header-actions">
|
||||
<a class="btn-login" href="/connexion.php">
|
||||
<span class="d-sm-none"><?php echo $translations['btn_se_connecter']?></span>
|
||||
<span class="d-none d-sm-inline"><?php echo $translations['ja_spec']?></span>
|
||||
</a>
|
||||
<!--<a class="btn-login" href="/connexion.php"><?php echo $translations['ja_spec']?></a>-->
|
||||
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-logo rounded-circle d-inline-flex align-items-center justify-content-center question-btn"
|
||||
type="button" data-bs-toggle="dropdown" data-bs-auto-close="true"
|
||||
aria-expanded="false" aria-label="Помощь">?</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end shadow help-menu menu-panel">
|
||||
<li><a href="/how-it-work.php"><?= $translations['kak_rabotaet'] ?></a></a></li>
|
||||
<li><a href="/about.php"><?= $translations['pro_nas'] ?></a></li>
|
||||
<li><a href="/faq.php"><?= $translations['faq'] ?></a></li>
|
||||
<li><a href="/contact.php"><?= $translations['nam_pisat'] ?></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="main">
|
||||
<div class="container py-4">
|
||||
<div class="profile-card">
|
||||
<div class="profile-grid profile-top">
|
||||
<!-- Cadre 1 : Infos -->
|
||||
<section class="panel">
|
||||
<div class="info-scroll">
|
||||
<h2 class="pro-name"><?= h($name) ?></h2>
|
||||
<?php if($p['specialite']): ?><div><strong><?= h($translations['dp_specialite']) ?>:</strong> <?= h($p['specialite']) ?></div><?php endif; ?>
|
||||
<!--<?php if($p['telephone']): ?><div><strong><?= h($translations['dp_telephone']) ?>:</strong> <?= h($p['telephone']) ?></div><?php endif; ?>-->
|
||||
<?php if($p['titre']): ?><div><strong><?= h($translations['tarif_consultation']) ?>:</strong> <?= h($p['titre']) ?></div><?php endif; ?>
|
||||
<?php if($p['disponibilite']): ?><div><strong><?= h($translations['moyens_payement']) ?>:</strong> <?= h($p['disponibilite']) ?></div><?php endif; ?>
|
||||
<?php if($p['adresse']): ?><div><strong><?= h($translations['dp_adresse']) ?>:</strong> <?= nl2br(h($p['adresse'])) ?></div><?php endif; ?>
|
||||
<?php if($p['ville'] || $p['code_postal']): ?><div><strong><?= h($translations['dp_ville_cp']) ?>:</strong> <?= h(trim(($p['ville']??'').' '.($p['code_postal']??''))) ?></div><?php endif; ?>
|
||||
<?php if($p['presentation']): ?><div class="mt-3"><strong><?= h($translations['dp_description']) ?>:</strong><br><?= nl2br(h($p['presentation'])) ?></div><?php endif; ?>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Centre : grande photo -->
|
||||
<section class="big-photo">
|
||||
<img class="img-fluid w-100" src="<?= h($photo) ?>" alt="Photo">
|
||||
</section>
|
||||
|
||||
<!-- Cadre 2 : Vidéos -->
|
||||
<?php if($p['video'] != ''){ ?>
|
||||
<aside class="panel">
|
||||
<h2 class="pro-name"><?= h($translations['dp_videos']) ?></h2>
|
||||
<div class="vlist">
|
||||
<?php
|
||||
$alt = h($translations['video_alt'] ?? 'Vidéo');
|
||||
$printed = false;
|
||||
|
||||
// Petite fonction d'affichage d’une vignette
|
||||
$printThumb = function($provider, $id, $thumb, $rawUrl) use ($alt){
|
||||
if (in_array($provider, ['youtube','dailymotion']) && $id){
|
||||
echo '<a href="#" class="vitem d-block pro-video"
|
||||
data-bs-toggle="modal" data-bs-target="#videoModal"
|
||||
data-provider="'.h($provider).'" data-id="'.h($id).'">';
|
||||
echo $thumb
|
||||
? '<img src="'.h($thumb).'" alt="'.$alt.'" loading="lazy">'
|
||||
: '<img src="/public/uploads/img/video_placeholder.jpg" alt="'.$alt.'" loading="lazy">';
|
||||
echo '</a>';
|
||||
} else {
|
||||
$safe = h($rawUrl);
|
||||
echo '<a class="vitem d-block" href="'.$safe.'" target="_blank" rel="noopener">';
|
||||
echo $thumb
|
||||
? '<img src="'.h($thumb).'" alt="'.$alt.'" loading="lazy">'
|
||||
: '<img src="/public/uploads/img/video_placeholder.jpg" alt="'.$alt.'" loading="lazy">';
|
||||
echo '</a>';
|
||||
}
|
||||
};
|
||||
|
||||
if (!empty($p['video'])) {
|
||||
$vi = video_thumb_info($p['video']);
|
||||
$thumb = $vi['thumb'] ?? null;
|
||||
$prov = $vi['provider'] ?? null;
|
||||
$vid = $vi['id'] ?? null;
|
||||
$printThumb($prov, $vid, $thumb, $p['video']);
|
||||
$printed = true;
|
||||
}
|
||||
foreach ($videos as $v) {
|
||||
$vi = video_thumb_info($v['url']);
|
||||
$thumb = $vi['thumb'] ?? null;
|
||||
$prov = $vi['provider'] ?? null;
|
||||
$vid = $vi['id'] ?? null;
|
||||
$printThumb($prov, $vid, $thumb, $v['url']);
|
||||
$printed = true;
|
||||
}
|
||||
if (!$printed) {
|
||||
echo '<div class="vitem"><img src="/public/uploads/img/video_placeholder.jpg" alt="'.$alt.'"></div>';
|
||||
//echo '<div class="text-muted small mt-1">'.h($translations['dp_aucune_video']).'</div>';
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
</aside>
|
||||
<?php } ?>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Ligne du bas : Note / Dernier avis / Bouton RDV -->
|
||||
<div class="bottom-grid mt-4">
|
||||
<!-- Cadre 3 : Note -->
|
||||
<div class="card shadow-sm p-3">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<?php $m=(float)($stats['m'] ?? 0); $c=(int)($stats['c'] ?? 0); $w=percent_from_5($m); ?>
|
||||
<div class="rating" aria-label="Note moyenne">
|
||||
<div class="stars-base">★★★★★</div>
|
||||
<div class="stars-fill" style="width:<?= $w ?>%">★★★★★</div>
|
||||
</div>
|
||||
<div>
|
||||
<div><strong><?= h($translations['avis_note_moy']) ?>:</strong> <?= $m ? number_format($m,1,',',' ') : '—' ?>/5</div>
|
||||
<div class="text-muted">(<?= $c.' '.h($translations['avis_total']) ?>)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cadre 4 : Dernier avis -->
|
||||
<div class="card shadow-sm p-3 avis-card">
|
||||
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||
<strong><?= h($translations['avis_dernier']) ?></strong>
|
||||
<a class="btn btn-link p-0" href="/avis_public.php?praticien_id=<?= (int)$praticien_id ?>">
|
||||
<?= h($translations['avis_lire_tous']) ?>
|
||||
</a>
|
||||
</div>
|
||||
<?php if ($lrow): ?>
|
||||
<?php $lw=percent_from_5((float)$lrow['note']); ?>
|
||||
<div class="rating mb-1">
|
||||
<div class="stars-base">★★★★★</div>
|
||||
<div class="stars-fill" style="width:<?= $lw ?>%">★★★★★</div>
|
||||
</div>
|
||||
<div class="meta">
|
||||
<?= h(pseudo_from($lrow['prenom'] ?? '', $lrow['nom'] ?? '')) ?>
|
||||
<?php if(!empty($lrow['visit_date'])): ?> • <?= date('d/m/Y', strtotime($lrow['visit_date'])) ?><?php endif; ?>
|
||||
</div>
|
||||
<div><?= nl2br(h(mb_strimwidth((string)$lrow['commentaire'],0,320,'…','UTF-8'))) ?></div>
|
||||
<?php else: ?>
|
||||
<div class="text-muted"><?= h($translations['avis_aucun']) ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- Bouton RDV -->
|
||||
<div class="text-end">
|
||||
<a class="btn btn-danger btn-lg" href="/public/client/reserver_rdv.php?praticien_id=<?= (int)$praticien_id ?>">
|
||||
<?= h($translations['dp_btn_rdv']) ?>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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"><?= h($translations['video_titre'] ?? 'Vidéo') ?></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="<?= h($translations['fermer'] ?? 'Fermer') ?>"></button>
|
||||
</div>
|
||||
<div class="modal-body pt-0">
|
||||
<div class="ratio ratio-16x9 rounded-3 overflow-hidden bg-black">
|
||||
<iframe id="vPlayer" src="" title="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">
|
||||
<?= h($translations['fermer'] ?? 'Fermer') ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function(){
|
||||
const modal = document.getElementById('videoModal');
|
||||
const iframe = document.getElementById('vPlayer');
|
||||
|
||||
function buildEmbed(provider, id){
|
||||
if (!provider || !id) return '';
|
||||
if (provider === 'youtube')
|
||||
return 'https://www.youtube-nocookie.com/embed/' + id + '?autoplay=1&rel=0&modestbranding=1';
|
||||
if (provider === 'dailymotion')
|
||||
return 'https://www.dailymotion.com/embed/video/' + id + '?autoplay=1&ui-start-screen-info=false';
|
||||
return '';
|
||||
}
|
||||
|
||||
document.addEventListener('click', function(e){
|
||||
const a = e.target.closest('a.pro-video[data-provider][data-id]');
|
||||
if (!a) return;
|
||||
e.preventDefault();
|
||||
const src = buildEmbed(a.dataset.provider, a.dataset.id);
|
||||
if (src) iframe.src = src;
|
||||
}, false);
|
||||
|
||||
modal.addEventListener('hidden.bs.modal', function(){
|
||||
iframe.src = '';
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" defer></script>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p><?= $translations['copyr'] ?></p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
+336
-140
@@ -1,87 +1,272 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../includes/bootstrap_global.php';
|
||||
require_once APP_HTDOCS . '/includes/avatar_initiales.php';
|
||||
$SPEC_CANON = require __DIR__ . '/includes/spec_map.php';
|
||||
declare (strict_types = 1);
|
||||
|
||||
$canon = $SPEC_CANON[$lang] ?? [];
|
||||
require_once __DIR__ . '/../includes/bootstrap_global.php';
|
||||
require_once APP_HTDOCS . '/includes/avatar_initiales.php';
|
||||
require_once __DIR__ . '/includes/search_helpers.php';
|
||||
|
||||
$SPEC_CANON = require __DIR__ . '/includes/spec_map.php';
|
||||
$canon = $SPEC_CANON[$lang] ?? [];
|
||||
$currentLang = $lang ?? 'ru';
|
||||
if (! in_array($currentLang, ['ru', 'fr', 'en'], true)) {
|
||||
$currentLang = 'ru';
|
||||
}
|
||||
|
||||
function h($s)
|
||||
{return htmlspecialchars((string) $s, ENT_QUOTES, 'UTF-8');}
|
||||
function percent_from_5($note)
|
||||
{$n = max(0, min(5, (float) $note));return (int) round($n / 5 * 100);}
|
||||
function pseudo_from($prenom, $nom)
|
||||
{
|
||||
$p = trim((string) $prenom);
|
||||
$n = trim((string) $nom);
|
||||
$p = $p !== '' ? mb_convert_case($p, MB_CASE_TITLE, 'UTF-8') : 'Client';
|
||||
$i = $n !== '' ? mb_strtoupper(mb_substr($nom, 0, 1, 'UTF-8'), 'UTF-8') : '';
|
||||
return $i ? ($p . '.' . $i) : $p;
|
||||
}
|
||||
function video_thumb_info(?string $url): ?array
|
||||
{
|
||||
if (! $url || ! trim($url)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// ------- (ton code existant) -------
|
||||
$praticien_id = (int)($_GET['id'] ?? 0);
|
||||
if ($praticien_id <= 0) { echo "<div class='container py-5'>ID invalide.</div>"; exit; }
|
||||
$u = trim($url);
|
||||
|
||||
function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
|
||||
function percent_from_5($note){ $n = max(0,min(5,(float)$note)); return (int)round($n/5*100); }
|
||||
function pseudo_from($prenom,$nom){
|
||||
$p = trim((string)$prenom); $n = trim((string)$nom);
|
||||
$p = $p!=='' ? mb_convert_case($p, MB_CASE_TITLE, 'UTF-8') : 'Client';
|
||||
$i = $n!=='' ? mb_strtoupper(mb_substr($nom,0,1,'UTF-8'),'UTF-8') : '';
|
||||
return $i ? ($p.'.'.$i) : $p;
|
||||
}
|
||||
function video_thumb_info(?string $url): ?array {
|
||||
if (!$url || !trim($url)) return null; $u = trim($url);
|
||||
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"];
|
||||
}
|
||||
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"];
|
||||
}
|
||||
$path=parse_url($u,PHP_URL_PATH)??''; if(preg_match('~\.(mp4|webm|ogg)$~i',$path)){ return ['provider'=>'file','id'=>null,'thumb'=>null]; }
|
||||
if (stripos($u,'vimeo.com')!==false){ return ['provider'=>'vimeo','id'=>null,'thumb'=>null]; }
|
||||
return ['provider'=>'other','id'=>null,'thumb'=>null];
|
||||
}
|
||||
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"];
|
||||
}
|
||||
|
||||
// 1) Praticien
|
||||
$st = $pdo->prepare("SELECT id, titre, nom, prenom, telephone, adresse, ville, code_postal, specialite, presentation, photo, video, disponibilite
|
||||
FROM praticien WHERE id = ?");
|
||||
$st->execute([$praticien_id]);
|
||||
$p = $st->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$p) { echo "<div class='container py-5'>".$translations['praticien_introuvable']."</div>"; exit; }
|
||||
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"];
|
||||
}
|
||||
|
||||
$name = $name = trim($p['prenom'] . ' ' . $p['nom']);
|
||||
$photo = !empty($p['photo']) ? "/public/uploads/".ltrim($p['photo'],'/') : "/public/uploads/img/user_placeholder.png";
|
||||
$path = parse_url($u, PHP_URL_PATH) ?? '';
|
||||
if (preg_match('~\.(mp4|webm|ogg)$~i', $path)) {
|
||||
return ['provider' => 'file', 'id' => null, 'thumb' => null];
|
||||
}
|
||||
|
||||
// 2) Vidéos additionnelles
|
||||
$vs = $pdo->prepare("SELECT id, url, title, ordre FROM praticien_video WHERE praticien_id=? ORDER BY ordre ASC, id DESC");
|
||||
$vs->execute([$praticien_id]);
|
||||
$videos = $vs->fetchAll(PDO::FETCH_ASSOC);
|
||||
if (stripos($u, 'vimeo.com') !== false) {
|
||||
return ['provider' => 'vimeo', 'id' => null, 'thumb' => null];
|
||||
}
|
||||
|
||||
// 3) Avis: moyenne + total + dernier
|
||||
$avg = $pdo->prepare("SELECT ROUND(AVG(note),1) AS m, COUNT(*) AS c FROM avis_praticien WHERE praticien_id=? AND statut='publie'");
|
||||
$avg->execute([$praticien_id]);
|
||||
$stats = $avg->fetch(PDO::FETCH_ASSOC) ?: ['m'=>null,'c'=>0];
|
||||
return ['provider' => 'other', 'id' => null, 'thumb' => null];
|
||||
}
|
||||
|
||||
$last = $pdo->prepare("SELECT a.note,a.titre,a.commentaire,a.visit_date,a.created_at,c.prenom,c.nom
|
||||
FROM avis_praticien a
|
||||
LEFT JOIN client c ON c.id=a.client_id
|
||||
WHERE a.praticien_id=? AND a.statut='publie'
|
||||
ORDER BY a.created_at DESC LIMIT 1");
|
||||
$last->execute([$praticien_id]);
|
||||
$lrow = $last->fetch(PDO::FETCH_ASSOC);
|
||||
// ID praticien
|
||||
$praticien_id = (int) ($_GET['id'] ?? 0);
|
||||
if ($praticien_id <= 0) {
|
||||
echo "<div class='container py-5'>ID invalide.</div>";
|
||||
exit;
|
||||
}
|
||||
|
||||
// 1) Praticien
|
||||
$st = $pdo->prepare("
|
||||
SELECT
|
||||
p.id,
|
||||
p.titre,
|
||||
p.nom,
|
||||
p.prenom,
|
||||
p.telephone,
|
||||
p.adresse,
|
||||
p.ville,
|
||||
p.code_postal,
|
||||
p.specialite,
|
||||
p.presentation,
|
||||
p.photo,
|
||||
p.video,
|
||||
p.disponibilite,
|
||||
COALESCE(pt.nom_affiche, p.nom) AS nom_affiche,
|
||||
COALESCE(pt.prenom_affiche, p.prenom) AS prenom_affiche,
|
||||
COALESCE(pt.ville_affichee, p.ville) AS ville_affichee,
|
||||
COALESCE(pt.titre, p.titre) AS titre_affiche,
|
||||
COALESCE(pt.specialite, p.specialite) AS specialite_affichee,
|
||||
COALESCE(pt.presentation, p.presentation) AS presentation_affichee,
|
||||
COALESCE(pt.disponibilite, p.disponibilite) AS disponibilite_affichee
|
||||
FROM praticien p
|
||||
LEFT JOIN praticien_traduction pt
|
||||
ON pt.praticien_id = p.id
|
||||
AND pt.langue = :lang
|
||||
WHERE p.id = :id
|
||||
LIMIT 1
|
||||
");
|
||||
$st->execute([
|
||||
':lang' => $currentLang,
|
||||
':id' => $praticien_id,
|
||||
]);
|
||||
$p = $st->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (! $p) {
|
||||
echo "<div class='container py-5'>" . ($translations['praticien_introuvable'] ?? 'Praticien introuvable') . "</div>";
|
||||
exit;
|
||||
}
|
||||
|
||||
$name = trim(($p['prenom_affiche'] ?? $p['prenom'] ?? '') . ' ' . ($p['nom_affiche'] ?? $p['nom'] ?? ''));
|
||||
$photo = ! empty($p['photo'])
|
||||
? "/public/uploads/" . ltrim((string) $p['photo'], '/')
|
||||
: "/public/uploads/img/user_placeholder.png";
|
||||
|
||||
// Slug spécialité SEO depuis la valeur BDD
|
||||
$specSlug = '';
|
||||
|
||||
if (! empty($p['specialite'])) {
|
||||
$specDb = mb_strtolower(trim((string) $p['specialite']), 'UTF-8');
|
||||
|
||||
// 1) correspondance exacte
|
||||
foreach (seo_spec_map() as $slug => $cfg) {
|
||||
$dbVal = mb_strtolower(trim((string) ($cfg['db'] ?? '')), 'UTF-8');
|
||||
if ($dbVal !== '' && $dbVal === $specDb) {
|
||||
$specSlug = $slug;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 2) commence par
|
||||
if ($specSlug === '') {
|
||||
foreach (seo_spec_map() as $slug => $cfg) {
|
||||
$dbVal = mb_strtolower(trim((string) ($cfg['db'] ?? '')), 'UTF-8');
|
||||
if ($dbVal !== '' && mb_stripos($specDb, $dbVal, 0, 'UTF-8') === 0) {
|
||||
$specSlug = $slug;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3) contient
|
||||
if ($specSlug === '') {
|
||||
foreach (seo_spec_map() as $slug => $cfg) {
|
||||
$dbVal = mb_strtolower(trim((string) ($cfg['db'] ?? '')), 'UTF-8');
|
||||
if ($dbVal !== '' && mb_stripos($specDb, $dbVal, 0, 'UTF-8') !== false) {
|
||||
$specSlug = $slug;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4) fallback propre : slug du texte libre
|
||||
if ($specSlug === '') {
|
||||
$specSlug = seo_slug((string) $p['specialite']);
|
||||
}
|
||||
}
|
||||
|
||||
if ($specSlug === '') {
|
||||
$specSlug = 'specialiste';
|
||||
}
|
||||
|
||||
// Slug ville SEO
|
||||
$citySlug = '';
|
||||
if (! empty($p['ville'])) {
|
||||
$citySlug = seo_city_slug_from_input((string) $p['ville']);
|
||||
}
|
||||
if ($citySlug === '') {
|
||||
$citySlug = 'france';
|
||||
}
|
||||
|
||||
// Slug nom
|
||||
$nameSlug = seo_slug($name);
|
||||
if ($nameSlug === '') {
|
||||
$nameSlug = 'profil';
|
||||
}
|
||||
|
||||
// URL SEO finale
|
||||
$prettyPath = '/specialistes/' . rawurlencode($specSlug) . '/' . rawurlencode($citySlug) . '/' . rawurlencode($nameSlug . '-' . (int) $p['id']) . '/';
|
||||
$canonicalUrl = 'https://uslugi.fr' . $prettyPath;
|
||||
|
||||
// Redirection 301 depuis ancienne URL
|
||||
$currentPath = parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH) ?? '';
|
||||
if (
|
||||
($currentPath === '/detail_public.php' || $currentPath === '/specialistes')
|
||||
&& (int) ($_GET['id'] ?? 0) > 0
|
||||
) {
|
||||
$target = $prettyPath;
|
||||
|
||||
$extra = $_GET;
|
||||
unset($extra['id'], $extra['slug']);
|
||||
|
||||
if (! empty($extra)) {
|
||||
$target .= '?' . http_build_query($extra);
|
||||
}
|
||||
|
||||
if (($_SERVER['REQUEST_URI'] ?? '') !== $target) {
|
||||
header('Location: ' . $target, true, 301);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Variables SEO
|
||||
$seoTitle = $name;
|
||||
if (! empty($p['specialite_affichee'])) {
|
||||
$seoTitle .= ' — ' . $p['specialite_affichee'];
|
||||
}
|
||||
if (! empty($p['ville_affichee'])) {
|
||||
$seoTitle .= ' à ' . $p['ville_affichee'];
|
||||
}
|
||||
$seoTitle .= ' | Uslugi.fr';
|
||||
|
||||
$presentationShort = trim(strip_tags((string) ($p['presentation_affichee'] ?? $p['presentation'] ?? '')));
|
||||
if ($presentationShort !== '') {
|
||||
$presentationShort = mb_substr($presentationShort, 0, 155, 'UTF-8');
|
||||
}
|
||||
|
||||
$seoDesc = $presentationShort !== ''
|
||||
? $presentationShort
|
||||
: (
|
||||
$name
|
||||
. (! empty($p['specialite_affichee']) ? ' — ' . $p['specialite_affichee'] : '')
|
||||
. (! empty($p['ville_affichee']) ? ' à ' . $p['ville_affichee'] : '')
|
||||
. '.'
|
||||
);
|
||||
|
||||
// 2) Vidéos additionnelles
|
||||
$vs = $pdo->prepare("SELECT id, url, title, ordre FROM praticien_video WHERE praticien_id=? ORDER BY ordre ASC, id DESC");
|
||||
$vs->execute([$praticien_id]);
|
||||
$videos = $vs->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// 3) Avis: moyenne + total + dernier
|
||||
$avg = $pdo->prepare("SELECT ROUND(AVG(note),1) AS m, COUNT(*) AS c FROM avis_praticien WHERE praticien_id=? AND statut='publie'");
|
||||
$avg->execute([$praticien_id]);
|
||||
$stats = $avg->fetch(PDO::FETCH_ASSOC) ?: ['m' => null, 'c' => 0];
|
||||
|
||||
$last = $pdo->prepare("
|
||||
SELECT a.note, a.titre, a.commentaire, a.visit_date, a.created_at, c.prenom, c.nom
|
||||
FROM avis_praticien a
|
||||
LEFT JOIN client c ON c.id = a.client_id
|
||||
WHERE a.praticien_id = ? AND a.statut = 'publie'
|
||||
ORDER BY a.created_at DESC
|
||||
LIMIT 1
|
||||
");
|
||||
$last->execute([$praticien_id]);
|
||||
$lrow = $last->fetch(PDO::FETCH_ASSOC);
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<html lang="<?php echo h($currentLang) ?>">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title><?= $name . " - " . $p['specialite'] . " - " . htmlspecialchars($translations['seo_detail_public_title'], ENT_QUOTES, 'UTF-8')?></title>
|
||||
<meta name="description" content="<?= $name . " - " . $p['specialite'] . " - " . $p['presentation'] ?>">
|
||||
<meta name="keywords" content="<?= htmlspecialchars($translations['seo_detail_public_keywords'], ENT_QUOTES, 'UTF-8') ?>">
|
||||
<meta name="author" content="Uslugi.fr">
|
||||
<title><?php echo h($seoTitle) ?></title>
|
||||
<meta name="description" content="<?php echo h($seoDesc) ?>">
|
||||
<link rel="canonical" href="<?php echo h($canonicalUrl) ?>">
|
||||
<meta name="robots" content="index, follow">
|
||||
|
||||
<meta property="og:type" content="profile">
|
||||
<meta property="og:url" content="<?php echo h($canonicalUrl) ?>">
|
||||
<meta property="og:title" content="<?php echo h($seoTitle) ?>">
|
||||
<meta property="og:description" content="<?php echo h($seoDesc) ?>">
|
||||
<meta property="og:image" content="<?php echo h('https://uslugi.fr' . $photo) ?>">
|
||||
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
|
||||
<link href="/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="<?= asset('/includes/site_style.css') ?>">
|
||||
<link rel="stylesheet" href="<?= asset('/includes/custom_style.css') ?>">
|
||||
<link rel="stylesheet" href="<?= asset('/includes/mobil_style.css') ?>">
|
||||
<link rel="stylesheet" href="<?php echo asset('/includes/site_style.css') ?>">
|
||||
<link rel="stylesheet" href="<?php echo asset('/includes/custom_style.css') ?>">
|
||||
<link rel="stylesheet" href="<?php echo asset('/includes/mobil_style.css') ?>">
|
||||
<?php include_once __DIR__ . '/includes/tracking.php'; ?>
|
||||
</head>
|
||||
<body class="layout">
|
||||
|
||||
<?php if ($logged):
|
||||
include_header_clean(APP_HTDOCS . '/public/includes/header.php');
|
||||
include_header_clean(APP_HTDOCS . '/public/includes/header.php');
|
||||
else: ?>
|
||||
<!-- Header public (Connexion + “?”) -->
|
||||
<header class="site-header">
|
||||
@@ -102,20 +287,20 @@ else: ?>
|
||||
</div>
|
||||
<nav class="header-actions">
|
||||
<a class="btn-login" href="/connexion.php">
|
||||
<span class="d-sm-none"><?php echo $translations['btn_se_connecter']?></span>
|
||||
<span class="d-none d-sm-inline"><?php echo $translations['ja_spec']?></span>
|
||||
<span class="d-sm-none"><?php echo $translations['btn_se_connecter'] ?></span>
|
||||
<span class="d-none d-sm-inline"><?php echo $translations['ja_spec'] ?></span>
|
||||
</a>
|
||||
<!--<a class="btn-login" href="/connexion.php"><?php echo $translations['ja_spec']?></a>-->
|
||||
<!--<a class="btn-login" href="/connexion.php"><?php echo $translations['ja_spec'] ?></a>-->
|
||||
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-logo rounded-circle d-inline-flex align-items-center justify-content-center question-btn"
|
||||
type="button" data-bs-toggle="dropdown" data-bs-auto-close="true"
|
||||
aria-expanded="false" aria-label="Помощь">?</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end shadow help-menu menu-panel">
|
||||
<li><a href="/how-it-work.php"><?= $translations['kak_rabotaet'] ?></a></a></li>
|
||||
<li><a href="/about.php"><?= $translations['pro_nas'] ?></a></li>
|
||||
<li><a href="/faq.php"><?= $translations['faq'] ?></a></li>
|
||||
<li><a href="/contact.php"><?= $translations['nam_pisat'] ?></a></li>
|
||||
<li><a href="/how-it-work.php"><?php echo $translations['kak_rabotaet'] ?></a></a></li>
|
||||
<li><a href="/about.php"><?php echo $translations['pro_nas'] ?></a></li>
|
||||
<li><a href="/faq.php"><?php echo $translations['faq'] ?></a></li>
|
||||
<li><a href="/contact.php"><?php echo $translations['nam_pisat'] ?></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -130,75 +315,84 @@ else: ?>
|
||||
<!-- Cadre 1 : Infos -->
|
||||
<section class="panel">
|
||||
<div class="info-scroll">
|
||||
<h2 class="pro-name"><?= h($name) ?></h2>
|
||||
<?php if($p['specialite']): ?><div><strong><?= h($translations['dp_specialite']) ?>:</strong> <?= h($p['specialite']) ?></div><?php endif; ?>
|
||||
<!--<?php if($p['telephone']): ?><div><strong><?= h($translations['dp_telephone']) ?>:</strong> <?= h($p['telephone']) ?></div><?php endif; ?>-->
|
||||
<?php if($p['titre']): ?><div><strong><?= h($translations['tarif_consultation']) ?>:</strong> <?= h($p['titre']) ?></div><?php endif; ?>
|
||||
<?php if($p['disponibilite']): ?><div><strong><?= h($translations['moyens_payement']) ?>:</strong> <?= h($p['disponibilite']) ?></div><?php endif; ?>
|
||||
<?php if($p['adresse']): ?><div><strong><?= h($translations['dp_adresse']) ?>:</strong> <?= nl2br(h($p['adresse'])) ?></div><?php endif; ?>
|
||||
<?php if($p['ville'] || $p['code_postal']): ?><div><strong><?= h($translations['dp_ville_cp']) ?>:</strong> <?= h(trim(($p['ville']??'').' '.($p['code_postal']??''))) ?></div><?php endif; ?>
|
||||
<?php if($p['presentation']): ?><div class="mt-3"><strong><?= h($translations['dp_description']) ?>:</strong><br><?= nl2br(h($p['presentation'])) ?></div><?php endif; ?>
|
||||
<h2 class="pro-name"><?php echo h($name) ?></h2>
|
||||
<?php if ($p['specialite_affichee']): ?><div><strong><?php echo h($translations['dp_specialite']) ?>:</strong> <?php echo h($p['specialite_affichee']) ?></div><?php endif; ?>
|
||||
<!--<?php if ($p['telephone']): ?><div><strong><?php echo h($translations['dp_telephone']) ?>:</strong> <?php echo h($p['telephone']) ?></div><?php endif; ?>-->
|
||||
<?php if ($p['titre_affiche']): ?><div><strong><?php echo h($translations['tarif_consultation']) ?>:</strong> <?php echo h($p['titre_affiche']) ?></div><?php endif; ?>
|
||||
<?php if ($p['disponibilite_affichee']): ?><div><strong><?php echo h($translations['moyens_payement']) ?>:</strong> <?php echo h($p['disponibilite_affichee']) ?></div><?php endif; ?>
|
||||
<?php if ($p['adresse']): ?><div><strong><?php echo h($translations['dp_adresse']) ?>:</strong> <?php echo nl2br(h($p['adresse'])) ?></div><?php endif; ?>
|
||||
<?php if ($p['ville_affichee'] || $p['code_postal']): ?><div><strong><?php echo h($translations['dp_ville_cp']) ?>:</strong> <?php echo h(trim(($p['ville_affichee'] ?? '') . ' ' . ($p['code_postal'] ?? ''))) ?></div><?php endif; ?>
|
||||
<?php if ($p['presentation_affichee']): ?><div class="mt-3"><strong><?php echo h($translations['dp_description']) ?>:</strong><br><?php echo nl2br(h($p['presentation_affichee'])) ?></div><?php endif; ?>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Centre : grande photo -->
|
||||
<section class="big-photo">
|
||||
<img class="img-fluid w-100" src="<?= h($photo) ?>" alt="Photo">
|
||||
</section>
|
||||
<img class="img-fluid w-100"
|
||||
src="<?php echo h($photo) ?>"
|
||||
alt="<?php
|
||||
echo h(
|
||||
trim($name)
|
||||
. (! empty($p['specialite_affichee']) ? ' — ' . $p['specialite_affichee'] : '')
|
||||
. (! empty($p['ville_affichee']) ? ' à ' . $p['ville_affichee'] : '')
|
||||
. ' — Uslugi.fr'
|
||||
);
|
||||
?>">
|
||||
</section>
|
||||
|
||||
<!-- Cadre 2 : Vidéos -->
|
||||
<?php if($p['video'] != ''){ ?>
|
||||
<?php if ($p['video'] != '') {?>
|
||||
<aside class="panel">
|
||||
<h2 class="pro-name"><?= h($translations['dp_videos']) ?></h2>
|
||||
<h2 class="pro-name"><?php echo h($translations['dp_videos']) ?></h2>
|
||||
<div class="vlist">
|
||||
<?php
|
||||
$alt = h($translations['video_alt'] ?? 'Vidéo');
|
||||
$printed = false;
|
||||
$alt = h($translations['video_alt'] ?? 'Vidéo');
|
||||
$printed = false;
|
||||
|
||||
// Petite fonction d'affichage d’une vignette
|
||||
$printThumb = function($provider, $id, $thumb, $rawUrl) use ($alt){
|
||||
if (in_array($provider, ['youtube','dailymotion']) && $id){
|
||||
echo '<a href="#" class="vitem d-block pro-video"
|
||||
// Petite fonction d'affichage d’une vignette
|
||||
$printThumb = function ($provider, $id, $thumb, $rawUrl) use ($alt) {
|
||||
if (in_array($provider, ['youtube', 'dailymotion']) && $id) {
|
||||
echo '<a href="#" class="vitem d-block pro-video"
|
||||
data-bs-toggle="modal" data-bs-target="#videoModal"
|
||||
data-provider="'.h($provider).'" data-id="'.h($id).'">';
|
||||
echo $thumb
|
||||
? '<img src="'.h($thumb).'" alt="'.$alt.'" loading="lazy">'
|
||||
: '<img src="/public/uploads/img/video_placeholder.jpg" alt="'.$alt.'" loading="lazy">';
|
||||
echo '</a>';
|
||||
} else {
|
||||
$safe = h($rawUrl);
|
||||
echo '<a class="vitem d-block" href="'.$safe.'" target="_blank" rel="noopener">';
|
||||
echo $thumb
|
||||
? '<img src="'.h($thumb).'" alt="'.$alt.'" loading="lazy">'
|
||||
: '<img src="/public/uploads/img/video_placeholder.jpg" alt="'.$alt.'" loading="lazy">';
|
||||
echo '</a>';
|
||||
}
|
||||
};
|
||||
data-provider="' . h($provider) . '" data-id="' . h($id) . '">';
|
||||
echo $thumb
|
||||
? '<img src="' . h($thumb) . '" alt="' . $alt . '" loading="lazy">'
|
||||
: '<img src="/public/uploads/img/video_placeholder.jpg" alt="' . $alt . '" loading="lazy">';
|
||||
echo '</a>';
|
||||
} else {
|
||||
$safe = h($rawUrl);
|
||||
echo '<a class="vitem d-block" href="' . $safe . '" target="_blank" rel="noopener">';
|
||||
echo $thumb
|
||||
? '<img src="' . h($thumb) . '" alt="' . $alt . '" loading="lazy">'
|
||||
: '<img src="/public/uploads/img/video_placeholder.jpg" alt="' . $alt . '" loading="lazy">';
|
||||
echo '</a>';
|
||||
}
|
||||
};
|
||||
|
||||
if (!empty($p['video'])) {
|
||||
$vi = video_thumb_info($p['video']);
|
||||
$thumb = $vi['thumb'] ?? null;
|
||||
$prov = $vi['provider'] ?? null;
|
||||
$vid = $vi['id'] ?? null;
|
||||
$printThumb($prov, $vid, $thumb, $p['video']);
|
||||
$printed = true;
|
||||
}
|
||||
foreach ($videos as $v) {
|
||||
$vi = video_thumb_info($v['url']);
|
||||
$thumb = $vi['thumb'] ?? null;
|
||||
$prov = $vi['provider'] ?? null;
|
||||
$vid = $vi['id'] ?? null;
|
||||
$printThumb($prov, $vid, $thumb, $v['url']);
|
||||
$printed = true;
|
||||
}
|
||||
if (!$printed) {
|
||||
echo '<div class="vitem"><img src="/public/uploads/img/video_placeholder.jpg" alt="'.$alt.'"></div>';
|
||||
//echo '<div class="text-muted small mt-1">'.h($translations['dp_aucune_video']).'</div>';
|
||||
}
|
||||
?>
|
||||
if (! empty($p['video'])) {
|
||||
$vi = video_thumb_info($p['video']);
|
||||
$thumb = $vi['thumb'] ?? null;
|
||||
$prov = $vi['provider'] ?? null;
|
||||
$vid = $vi['id'] ?? null;
|
||||
$printThumb($prov, $vid, $thumb, $p['video']);
|
||||
$printed = true;
|
||||
}
|
||||
foreach ($videos as $v) {
|
||||
$vi = video_thumb_info($v['url']);
|
||||
$thumb = $vi['thumb'] ?? null;
|
||||
$prov = $vi['provider'] ?? null;
|
||||
$vid = $vi['id'] ?? null;
|
||||
$printThumb($prov, $vid, $thumb, $v['url']);
|
||||
$printed = true;
|
||||
}
|
||||
if (! $printed) {
|
||||
echo '<div class="vitem"><img src="/public/uploads/img/video_placeholder.jpg" alt="' . $alt . '"></div>';
|
||||
//echo '<div class="text-muted small mt-1">'.h($translations['dp_aucune_video']).'</div>';
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
</aside>
|
||||
<?php } ?>
|
||||
<?php }?>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -207,14 +401,16 @@ else: ?>
|
||||
<!-- Cadre 3 : Note -->
|
||||
<div class="card shadow-sm p-3">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<?php $m=(float)($stats['m'] ?? 0); $c=(int)($stats['c'] ?? 0); $w=percent_from_5($m); ?>
|
||||
<?php $m = (float) ($stats['m'] ?? 0);
|
||||
$c = (int) ($stats['c'] ?? 0);
|
||||
$w = percent_from_5($m); ?>
|
||||
<div class="rating" aria-label="Note moyenne">
|
||||
<div class="stars-base">★★★★★</div>
|
||||
<div class="stars-fill" style="width:<?= $w ?>%">★★★★★</div>
|
||||
<div class="stars-fill" style="width:<?php echo $w ?>%">★★★★★</div>
|
||||
</div>
|
||||
<div>
|
||||
<div><strong><?= h($translations['avis_note_moy']) ?>:</strong> <?= $m ? number_format($m,1,',',' ') : '—' ?>/5</div>
|
||||
<div class="text-muted">(<?= $c.' '.h($translations['avis_total']) ?>)</div>
|
||||
<div><strong><?php echo h($translations['avis_note_moy']) ?>:</strong> <?php echo $m ? number_format($m, 1, ',', ' ') : '—' ?>/5</div>
|
||||
<div class="text-muted">(<?php echo $c . ' ' . h($translations['avis_total']) ?>)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -222,31 +418,31 @@ else: ?>
|
||||
<!-- Cadre 4 : Dernier avis -->
|
||||
<div class="card shadow-sm p-3 avis-card">
|
||||
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||
<strong><?= h($translations['avis_dernier']) ?></strong>
|
||||
<a class="btn btn-link p-0" href="/avis_public.php?praticien_id=<?= (int)$praticien_id ?>">
|
||||
<?= h($translations['avis_lire_tous']) ?>
|
||||
<strong><?php echo h($translations['avis_dernier']) ?></strong>
|
||||
<a class="btn btn-link p-0" href="/avis_public.php?praticien_id=<?php echo (int) $praticien_id ?>">
|
||||
<?php echo h($translations['avis_lire_tous']) ?>
|
||||
</a>
|
||||
</div>
|
||||
<?php if ($lrow): ?>
|
||||
<?php $lw=percent_from_5((float)$lrow['note']); ?>
|
||||
<?php $lw = percent_from_5((float) $lrow['note']); ?>
|
||||
<div class="rating mb-1">
|
||||
<div class="stars-base">★★★★★</div>
|
||||
<div class="stars-fill" style="width:<?= $lw ?>%">★★★★★</div>
|
||||
<div class="stars-fill" style="width:<?php echo $lw ?>%">★★★★★</div>
|
||||
</div>
|
||||
<div class="meta">
|
||||
<?= h(pseudo_from($lrow['prenom'] ?? '', $lrow['nom'] ?? '')) ?>
|
||||
<?php if(!empty($lrow['visit_date'])): ?> • <?= date('d/m/Y', strtotime($lrow['visit_date'])) ?><?php endif; ?>
|
||||
<?php echo h(pseudo_from($lrow['prenom'] ?? '', $lrow['nom'] ?? '')) ?>
|
||||
<?php if (! empty($lrow['visit_date'])): ?> • <?php echo date('d/m/Y', strtotime($lrow['visit_date'])) ?><?php endif; ?>
|
||||
</div>
|
||||
<div><?= nl2br(h(mb_strimwidth((string)$lrow['commentaire'],0,320,'…','UTF-8'))) ?></div>
|
||||
<div><?php echo nl2br(h(mb_strimwidth((string) $lrow['commentaire'], 0, 320, '…', 'UTF-8'))) ?></div>
|
||||
<?php else: ?>
|
||||
<div class="text-muted"><?= h($translations['avis_aucun']) ?></div>
|
||||
<div class="text-muted"><?php echo h($translations['avis_aucun']) ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- Bouton RDV -->
|
||||
<div class="text-end">
|
||||
<a class="btn btn-danger btn-lg" href="/public/client/reserver_rdv.php?praticien_id=<?= (int)$praticien_id ?>">
|
||||
<?= h($translations['dp_btn_rdv']) ?>
|
||||
<a class="btn btn-danger btn-lg" href="/public/client/reserver_rdv.php?praticien_id=<?php echo (int) $praticien_id ?>">
|
||||
<?php echo h($translations['dp_btn_rdv']) ?>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -259,8 +455,8 @@ else: ?>
|
||||
<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"><?= h($translations['video_titre'] ?? 'Vidéo') ?></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="<?= h($translations['fermer'] ?? 'Fermer') ?>"></button>
|
||||
<h5 class="modal-title" id="videoModalLabel"><?php echo h($translations['video_titre'] ?? 'Vidéo') ?></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="<?php echo h($translations['fermer'] ?? 'Fermer') ?>"></button>
|
||||
</div>
|
||||
<div class="modal-body pt-0">
|
||||
<div class="ratio ratio-16x9 rounded-3 overflow-hidden bg-black">
|
||||
@@ -272,7 +468,7 @@ else: ?>
|
||||
</div>
|
||||
<div class="modal-footer border-0">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
<?= h($translations['fermer'] ?? 'Fermer') ?>
|
||||
<?php echo h($translations['fermer'] ?? 'Fermer') ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -309,7 +505,7 @@ else: ?>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p><?= $translations['copyr'] ?></p>
|
||||
<p><?php echo $translations['copyr'] ?></p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
|
||||
+538
@@ -0,0 +1,538 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
|
||||
if (! function_exists('t')) {
|
||||
function t(array $translations, string $key, string $fallback = ''): string
|
||||
{
|
||||
$val = $translations[$key] ?? '';
|
||||
return $val !== '' ? (string) $val : $fallback;
|
||||
}
|
||||
}
|
||||
|
||||
$form_index_status = $form_index_status ?? null;
|
||||
$form_index_message = $form_index_message ?? '';
|
||||
$form_index_errors = $form_index_errors ?? [];
|
||||
$form_index_old = $form_index_old ?? [
|
||||
'name' => '',
|
||||
'category' => '',
|
||||
'situation' => '',
|
||||
'contact_type' => 'email',
|
||||
'email' => '',
|
||||
'wa' => '',
|
||||
];
|
||||
?>
|
||||
|
||||
<section class="concierge-section" id="concierge-form">
|
||||
<div class="concierge-inner">
|
||||
|
||||
<div class="concierge-heading">
|
||||
<span class="label"><?php echo htmlspecialchars(t($translations, 'form_index_badge', 'Бесплатно · Без регистрации'), ENT_QUOTES, 'UTF-8'); ?></span>
|
||||
<h2><?php echo htmlspecialchars(t($translations, 'form_index_title', 'Не можете найти нужного специалиста?'), ENT_QUOTES, 'UTF-8'); ?></h2>
|
||||
<p><?php echo htmlspecialchars(t($translations, 'form_index_intro', 'Опишите вашу ситуацию — мы сами подберём подходящего русскоязычного специалиста во Франции и свяжемся с вами.'), ENT_QUOTES, 'UTF-8'); ?></p>
|
||||
</div>
|
||||
|
||||
<div class="concierge-card">
|
||||
<div class="card-stripe"></div>
|
||||
|
||||
<?php if ($form_index_status !== 'success'): ?>
|
||||
<div class="card-form" id="formState">
|
||||
|
||||
<?php if ($form_index_status === 'error' && $form_index_message !== ''): ?>
|
||||
<div class="form-alert form-alert-error">
|
||||
<?php echo htmlspecialchars($form_index_message, ENT_QUOTES, 'UTF-8'); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="post" action="">
|
||||
<input type="hidden" name="form_index" value="1">
|
||||
|
||||
<div style="position:absolute;left:-9999px;opacity:0;pointer-events:none;" aria-hidden="true">
|
||||
<label for="form_index_company">Company</label>
|
||||
<input type="text" name="company" id="form_index_company" tabindex="-1" autocomplete="off">
|
||||
</div>
|
||||
|
||||
<div class="fields-grid">
|
||||
|
||||
<div class="field">
|
||||
<label for="f-name">
|
||||
<?php echo htmlspecialchars(t($translations, 'form_index_name_label', 'Ваше имя'), ENT_QUOTES, 'UTF-8'); ?>
|
||||
<span class="req">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="f-name"
|
||||
name="name"
|
||||
placeholder="<?php echo htmlspecialchars(t($translations, 'form_index_name_placeholder', 'Как вас зовут?'), ENT_QUOTES, 'UTF-8'); ?>"
|
||||
autocomplete="given-name"
|
||||
value="<?php echo htmlspecialchars($form_index_old['name'], ENT_QUOTES, 'UTF-8'); ?>"
|
||||
class="<?php echo ! empty($form_index_errors['name']) ? 'error' : ''; ?>"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="f-category">
|
||||
<?php echo htmlspecialchars(t($translations, 'form_index_category_label', 'Нужен специалист'), ENT_QUOTES, 'UTF-8'); ?>
|
||||
<span class="req">*</span>
|
||||
</label>
|
||||
<select id="f-category" name="category" class="<?php echo ! empty($form_index_errors['category']) ? 'error' : ''; ?>">
|
||||
<option value=""><?php echo htmlspecialchars(t($translations, 'form_index_category_placeholder', 'Выберите категорию'), ENT_QUOTES, 'UTF-8'); ?></option>
|
||||
|
||||
<?php
|
||||
$categories = [
|
||||
'lawyer' => 'form_index_category_lawyer',
|
||||
'notary' => 'form_index_category_notary',
|
||||
'real_estate' => 'form_index_category_real_estate',
|
||||
'psychologist' => 'form_index_category_psychologist',
|
||||
'dentist_ortho' => 'form_index_category_dentist_ortho',
|
||||
'financial_advisor' => 'form_index_category_financial_advisor',
|
||||
'translator' => 'form_index_category_translator',
|
||||
'residence_consultant' => 'form_index_category_residence_consultant',
|
||||
'repair' => 'form_index_category_repair',
|
||||
'funeral' => 'form_index_category_funeral',
|
||||
'other' => 'form_index_category_other',
|
||||
];
|
||||
foreach ($categories as $value => $key):
|
||||
?>
|
||||
<option value="<?php echo htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); ?>" <?php echo $form_index_old['category'] === $value ? 'selected' : ''; ?>>
|
||||
<?php echo htmlspecialchars(t($translations, $key, $value), ENT_QUOTES, 'UTF-8'); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="field field-full">
|
||||
<label for="f-situation">
|
||||
<?php echo htmlspecialchars(t($translations, 'form_index_situation_label', 'Опишите вашу ситуацию'), ENT_QUOTES, 'UTF-8'); ?>
|
||||
<span class="req">*</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="f-situation"
|
||||
name="situation"
|
||||
placeholder="<?php echo htmlspecialchars(t($translations, 'form_index_situation_placeholder', 'Расскажите кратко — что случилось, что нужно сделать, в каком городе вы находитесь…'), ENT_QUOTES, 'UTF-8'); ?>"
|
||||
class="<?php echo ! empty($form_index_errors['situation']) ? 'error' : ''; ?>"
|
||||
><?php echo htmlspecialchars($form_index_old['situation'], ENT_QUOTES, 'UTF-8'); ?></textarea>
|
||||
</div>
|
||||
|
||||
<div class="field field-full">
|
||||
<div class="contact-label-row">
|
||||
<label>
|
||||
<?php echo htmlspecialchars(t($translations, 'form_index_contact_label', 'Как с вами связаться'), ENT_QUOTES, 'UTF-8'); ?>
|
||||
<span class="req">*</span>
|
||||
</label>
|
||||
<div class="contact-tabs">
|
||||
<button class="ctab <?php echo $form_index_old['contact_type'] === 'email' ? 'active' : ''; ?>" type="button" data-contact-tab="email">
|
||||
<?php echo htmlspecialchars(t($translations, 'form_index_contact_tab_email', 'Email'), ENT_QUOTES, 'UTF-8'); ?>
|
||||
</button>
|
||||
<button class="ctab <?php echo $form_index_old['contact_type'] === 'wa' ? 'active' : ''; ?>" type="button" data-contact-tab="wa">
|
||||
<?php echo htmlspecialchars(t($translations, 'form_index_contact_tab_wa', 'WhatsApp'), ENT_QUOTES, 'UTF-8'); ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="contact_type" id="contact_type" value="<?php echo htmlspecialchars($form_index_old['contact_type'], ENT_QUOTES, 'UTF-8'); ?>">
|
||||
|
||||
<div class="contact-input <?php echo $form_index_old['contact_type'] === 'email' ? 'show' : ''; ?>" id="ci-email">
|
||||
<input
|
||||
type="email"
|
||||
id="f-email"
|
||||
name="email"
|
||||
placeholder="<?php echo htmlspecialchars(t($translations, 'form_index_email_placeholder', 'votre@email.com'), ENT_QUOTES, 'UTF-8'); ?>"
|
||||
autocomplete="email"
|
||||
value="<?php echo htmlspecialchars($form_index_old['email'], ENT_QUOTES, 'UTF-8'); ?>"
|
||||
class="<?php echo ! empty($form_index_errors['email']) ? 'error' : ''; ?>"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="contact-input <?php echo $form_index_old['contact_type'] === 'wa' ? 'show' : ''; ?>" id="ci-wa">
|
||||
<input
|
||||
type="tel"
|
||||
id="f-wa"
|
||||
name="wa"
|
||||
placeholder="<?php echo htmlspecialchars(t($translations, 'form_index_wa_placeholder', '+33 6 00 00 00 00'), ENT_QUOTES, 'UTF-8'); ?>"
|
||||
autocomplete="tel"
|
||||
value="<?php echo htmlspecialchars($form_index_old['wa'], ENT_QUOTES, 'UTF-8'); ?>"
|
||||
class="<?php echo ! empty($form_index_errors['wa']) ? 'error' : ''; ?>"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<button class="btn-submit" type="submit">
|
||||
<?php echo htmlspecialchars(t($translations, 'form_index_submit', 'Подобрать специалиста'), ENT_QUOTES, 'UTF-8'); ?>
|
||||
|
||||
</button>
|
||||
|
||||
<p class="form-note">
|
||||
<?php echo htmlspecialchars(t($translations, 'form_index_note', 'Мы не передаём ваши данные третьим лицам и свяжемся с вами в течение нескольких часов.'), ENT_QUOTES, 'UTF-8'); ?>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="card-success" id="successState" style="display:block;">
|
||||
<div class="success-circle">✓</div>
|
||||
<h3><?php echo htmlspecialchars(t($translations, 'form_index_success_title', 'Заявка получена!'), ENT_QUOTES, 'UTF-8'); ?></h3>
|
||||
<p><?php echo htmlspecialchars(t($translations, 'form_index_success_text', 'Мы изучим вашу ситуацию и свяжемся с вами в ближайшее время, чтобы предложить подходящего специалиста.'), ENT_QUOTES, 'UTF-8'); ?></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.concierge-section {
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
background: #f7f8fc;
|
||||
padding: 52px 20px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.concierge-inner { max-width: 700px; margin: 0 auto; }
|
||||
|
||||
.concierge-heading { text-align: center; margin-bottom: 32px; }
|
||||
|
||||
.concierge-heading .label {
|
||||
display: inline-block;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: #2e4db0;
|
||||
background: #eef2fb;
|
||||
padding: 5px 14px;
|
||||
border-radius: 20px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.concierge-heading h2 {
|
||||
font-size: clamp(20px, 4vw, 26px);
|
||||
font-weight: 700;
|
||||
color: #1e2d5a;
|
||||
line-height: 1.3;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.concierge-heading p {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #6b7280;
|
||||
line-height: 1.65;
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.concierge-card {
|
||||
background: #fff;
|
||||
border-radius: 20px;
|
||||
border: 1.5px solid #dde3f0;
|
||||
box-shadow: 0 4px 24px rgba(30,45,90,0.08);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-stripe {
|
||||
height: 4px;
|
||||
background: linear-gradient(90deg, #1e2d5a 0%, #2e4db0 100%);
|
||||
}
|
||||
|
||||
.card-form { padding: 32px 36px 36px; }
|
||||
|
||||
.fields-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.field-full { grid-column: 1 / -1; }
|
||||
|
||||
.field { display: flex; flex-direction: column; gap: 7px; }
|
||||
|
||||
.field label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #1e2d5a;
|
||||
letter-spacing: 0.03em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.field label .req { color: #2e4db0; margin-left: 2px; }
|
||||
|
||||
.card-form input,
|
||||
.card-form textarea,
|
||||
.card-form select {
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #1e2d5a;
|
||||
background: #f7f8fc;
|
||||
border: 1.5px solid #dde3f0;
|
||||
border-radius: 8px;
|
||||
padding: 11px 14px;
|
||||
outline: none;
|
||||
transition: border-color 0.18s, background 0.18s, box-shadow 0.18s;
|
||||
width: 100%;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.card-form input::placeholder,
|
||||
.card-form textarea::placeholder { color: #b0b8cc; font-weight: 300; }
|
||||
|
||||
.card-form input:focus,
|
||||
.card-form textarea:focus,
|
||||
.card-form select:focus {
|
||||
border-color: #2e4db0;
|
||||
background: #fff;
|
||||
box-shadow: 0 0 0 3px rgba(46,77,176,0.09);
|
||||
}
|
||||
|
||||
.card-form input.error,
|
||||
.card-form textarea.error,
|
||||
.card-form select.error {
|
||||
border-color: #d73a3a;
|
||||
background: #fff8f8;
|
||||
}
|
||||
|
||||
.card-form textarea {
|
||||
resize: vertical;
|
||||
min-height: 100px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.card-form select {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='11' height='7' fill='none'%3E%3Cpath d='M1 1l4.5 4.5L10 1' stroke='%231e2d5a' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 14px center;
|
||||
padding-right: 38px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.contact-label-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 7px;
|
||||
}
|
||||
|
||||
.contact-tabs {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
background: #f7f8fc;
|
||||
border: 1.5px solid #dde3f0;
|
||||
border-radius: 6px;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
.ctab {
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
padding: 4px 10px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
background: none;
|
||||
color: #6b7280;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.ctab.active {
|
||||
background: #1e2d5a;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.contact-input { display: none; }
|
||||
.contact-input.show { display: flex; flex-direction: column; gap: 7px; }
|
||||
|
||||
.btn-submit {
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
background: #e03c3f;
|
||||
color: #fff;
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
padding: 14px 24px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
transition: background 0.18s, transform 0.12s, box-shadow 0.18s;
|
||||
}
|
||||
|
||||
.btn-submit:hover {
|
||||
background: #e03c3f;
|
||||
box-shadow: 0 6px 20px rgba(30,45,90,0.22);
|
||||
|
||||
}
|
||||
|
||||
.btn-submit:active { transform: translateY(0); box-shadow: none; }
|
||||
|
||||
.btn-icon {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255,255,255,0.18);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
transition: transform 0.18s;
|
||||
}
|
||||
|
||||
.btn-submit:hover .btn-icon { transform: translateX(3px); }
|
||||
|
||||
.form-note {
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
color: #6b7280;
|
||||
margin-top: 12px;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.form-alert {
|
||||
margin-bottom: 18px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.form-alert-error {
|
||||
background: #fff4f4;
|
||||
border: 1px solid #f0caca;
|
||||
color: #9f1d1d;
|
||||
}
|
||||
|
||||
.card-success {
|
||||
display: none;
|
||||
padding: 52px 36px;
|
||||
text-align: center;
|
||||
animation: fadeIn 0.35s ease both;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(12px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.success-circle {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 50%;
|
||||
background: #1e2d5a;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 18px;
|
||||
font-size: 24px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.card-success h3 {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #1e2d5a;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.card-success p {
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
line-height: 1.65;
|
||||
max-width: 380px;
|
||||
margin: 0 auto;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@media (max-width: 540px) {
|
||||
.card-form { padding: 24px 20px 28px; }
|
||||
.fields-grid { grid-template-columns: 1fr; }
|
||||
.card-success { padding: 36px 20px; }
|
||||
.concierge-section { padding: 36px 16px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const tabs = document.querySelectorAll('[data-contact-tab]');
|
||||
const typeInput = document.getElementById('contact_type');
|
||||
const emailWrap = document.getElementById('ci-email');
|
||||
const waWrap = document.getElementById('ci-wa');
|
||||
|
||||
function switchTab(type) {
|
||||
typeInput.value = type;
|
||||
|
||||
tabs.forEach(btn => {
|
||||
btn.classList.toggle('active', btn.dataset.contactTab === type);
|
||||
});
|
||||
|
||||
emailWrap.classList.toggle('show', type === 'email');
|
||||
waWrap.classList.toggle('show', type === 'wa');
|
||||
}
|
||||
|
||||
tabs.forEach(btn => {
|
||||
btn.addEventListener('click', function () {
|
||||
switchTab(this.dataset.contactTab);
|
||||
});
|
||||
});
|
||||
|
||||
switchTab(typeInput.value || 'email');
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const tabs = document.querySelectorAll('[data-contact-tab]');
|
||||
const typeInput = document.getElementById('contact_type');
|
||||
const emailWrap = document.getElementById('ci-email');
|
||||
const waWrap = document.getElementById('ci-wa');
|
||||
|
||||
function switchTab(type) {
|
||||
typeInput.value = type;
|
||||
|
||||
tabs.forEach(btn => {
|
||||
btn.classList.toggle('active', btn.dataset.contactTab === type);
|
||||
});
|
||||
|
||||
emailWrap.classList.toggle('show', type === 'email');
|
||||
waWrap.classList.toggle('show', type === 'wa');
|
||||
}
|
||||
|
||||
tabs.forEach(btn => {
|
||||
btn.addEventListener('click', function () {
|
||||
switchTab(this.dataset.contactTab);
|
||||
});
|
||||
});
|
||||
|
||||
switchTab(typeInput.value || 'email');
|
||||
});
|
||||
</script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
|
||||
<?php if ($form_index_status === 'success' || $form_index_status === 'error'): ?>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
Swal.fire({
|
||||
icon: <?php echo json_encode($form_index_status === 'success' ? 'success' : 'error'); ?>,
|
||||
title: <?php
|
||||
echo json_encode(
|
||||
$form_index_status === 'success'
|
||||
? t($translations, 'form_index_success_title', 'Заявка получена!')
|
||||
: t($translations, 'form_index_error_popup_title', 'Ошибка')
|
||||
);
|
||||
?>,
|
||||
text: <?php echo json_encode($form_index_message); ?>,
|
||||
confirmButtonText: 'OK',
|
||||
confirmButtonColor: '#1e2d5a'
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
@@ -910,3 +910,34 @@ a {
|
||||
border-color: #e03c3f;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.menu-item-form {
|
||||
padding: 0 !important;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.menu-logout-form {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.menu-logout-btn {
|
||||
all: unset;
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.menu-logout-btn:hover {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.menu-logout-btn:focus-visible {
|
||||
outline: 2px solid #94a3b8;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
// Évite la boucle quand router.php inclut la page
|
||||
if (defined('ROUTED_PRETTY')) return;
|
||||
|
||||
// Mapping vilain -> joli
|
||||
$UGLY_TO_PRETTY = [
|
||||
//racine
|
||||
'/index.php' => '/',
|
||||
'/result_search.php' => '/recherche',
|
||||
'/detail_public.php' => '/specialistes',
|
||||
'/connexion.php' => '/login',
|
||||
'/choix_inscr.php' => '/choix-inscription',
|
||||
'/inscription_client.php' => '/inscription-client',
|
||||
'/inscription_praticien.php' => '/inscription-praticien',
|
||||
'/mdp_oublie.php' => '/mot-de-passe-oublie',
|
||||
'/avis_public.php' => '/avis',
|
||||
//dossier client
|
||||
'/public/client/informations.php' => '/profil',
|
||||
'/public/client/modifier.php' => '/modifier-profil',
|
||||
'/public/client/compte.php' => '/mon-compte',
|
||||
'/public/client/list_praticien.php' => '/trouver-specialistes',
|
||||
'/public/client/mes_favori.php' => '/mes-preferences',
|
||||
'/public/client/detail_praticien.php' => '/specialiste',
|
||||
'/public/client/avis.php' => '/donner-avis',
|
||||
'/public/client/reserver_rdv.php' => '/reservation',
|
||||
'/public/client/mes_rendez_vous.php' => '/mes-rendez-vous',
|
||||
|
||||
//Dossier praticien
|
||||
'/public/praticien/informations.php' => '/profil-pro',
|
||||
'/public/praticien/modifier.php' => '/modifier-profil-pro',
|
||||
'/public/praticien/compte.php' => '/mon-compte-pro',
|
||||
'/public/praticien/disponibilites.php' => '/mes-creneaux',
|
||||
'/public/praticien/disponibilites_date.php' => '/mes-creneaux-date',
|
||||
'/public/praticien/rendez_vous.php' => '/mes-consultations',
|
||||
'/public/praticien/rendez_vous_du_jour.php' => '/consultations-du-jour',
|
||||
'/public/praticien/agenda.php' => '/agenda',
|
||||
'/public/praticien/synchronisation.php' => '/connecter-agenda',
|
||||
'/public/praticien/resiliation.php' => '/abonnement',
|
||||
'/public/praticien/factures.php' => '/mes-factures',
|
||||
|
||||
//Dossier messages
|
||||
|
||||
];
|
||||
|
||||
// Debug : ajouter ?_noredir=1 pour désactiver la redirection
|
||||
if (!empty($_GET['_noredir'])) return;
|
||||
|
||||
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) ?? '/';
|
||||
|
||||
if (isset($UGLY_TO_PRETTY[$path])) {
|
||||
$target = $UGLY_TO_PRETTY[$path];
|
||||
$qs = $_SERVER['QUERY_STRING'] ? ('?' . $_SERVER['QUERY_STRING']) : '';
|
||||
header('Location: ' . $target . $qs, true, 301);
|
||||
exit;
|
||||
}
|
||||
+82
-22
@@ -1,29 +1,30 @@
|
||||
<?php
|
||||
// Évite la boucle quand router.php inclut la page
|
||||
if (defined('ROUTED_PRETTY')) return;
|
||||
if (defined('ROUTED_PRETTY')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Mapping vilain -> joli
|
||||
$UGLY_TO_PRETTY = [
|
||||
//racine
|
||||
'/index.php' => '/',
|
||||
'/result_search.php' => '/recherche',
|
||||
'/detail_public.php' => '/specialistes',
|
||||
'/connexion.php' => '/login',
|
||||
'/choix_inscr.php' => '/choix-inscription',
|
||||
'/inscription_client.php' => '/inscription-client',
|
||||
'/inscription_praticien.php' => '/inscription-praticien',
|
||||
'/mdp_oublie.php' => '/mot-de-passe-oublie',
|
||||
'/avis_public.php' => '/avis',
|
||||
'/index.php' => '/',
|
||||
'/detail_public.php' => '/specialistes',
|
||||
'/connexion.php' => '/login',
|
||||
'/choix_inscr.php' => '/choix-inscription',
|
||||
'/inscription_client.php' => '/inscription-client',
|
||||
'/inscription_praticien.php' => '/inscription-praticien',
|
||||
'/mdp_oublie.php' => '/mot-de-passe-oublie',
|
||||
'/avis_public.php' => '/avis',
|
||||
//dossier client
|
||||
'/public/client/informations.php' => '/profil',
|
||||
'/public/client/modifier.php' => '/modifier-profil',
|
||||
'/public/client/compte.php' => '/mon-compte',
|
||||
'/public/client/list_praticien.php' => '/trouver-specialistes',
|
||||
'/public/client/mes_favori.php' => '/mes-preferences',
|
||||
'/public/client/detail_praticien.php' => '/specialiste',
|
||||
'/public/client/avis.php' => '/donner-avis',
|
||||
'/public/client/reserver_rdv.php' => '/reservation',
|
||||
'/public/client/mes_rendez_vous.php' => '/mes-rendez-vous',
|
||||
'/public/client/informations.php' => '/profil',
|
||||
'/public/client/modifier.php' => '/modifier-profil',
|
||||
'/public/client/compte.php' => '/mon-compte',
|
||||
'/public/client/list_praticien.php' => '/trouver-specialistes',
|
||||
'/public/client/mes_favori.php' => '/mes-preferences',
|
||||
'/public/client/detail_praticien.php' => '/specialiste',
|
||||
'/public/client/avis.php' => '/donner-avis',
|
||||
'/public/client/reserver_rdv.php' => '/reservation',
|
||||
'/public/client/mes_rendez_vous.php' => '/mes-rendez-vous',
|
||||
|
||||
//Dossier praticien
|
||||
'/public/praticien/informations.php' => '/profil-pro',
|
||||
@@ -37,15 +38,74 @@ $UGLY_TO_PRETTY = [
|
||||
'/public/praticien/synchronisation.php' => '/connecter-agenda',
|
||||
'/public/praticien/resiliation.php' => '/abonnement',
|
||||
'/public/praticien/factures.php' => '/mes-factures',
|
||||
|
||||
|
||||
//Dossier messages
|
||||
|
||||
|
||||
];
|
||||
|
||||
// Debug : ajouter ?_noredir=1 pour désactiver la redirection
|
||||
if (!empty($_GET['_noredir'])) return;
|
||||
if (! empty($_GET['_noredir'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) ?? '/';
|
||||
require_once __DIR__ . '/search_helpers.php';
|
||||
|
||||
// Redirection SEO pour result_search.php
|
||||
if ($path === '/result_search.php' || $path === '/recherche') {
|
||||
if (! empty($_GET['ajax'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$specialite = trim((string) ($_GET['specialite'] ?? ''));
|
||||
$ville = trim((string) ($_GET['ville'] ?? ''));
|
||||
|
||||
if ($specialite !== '' || $ville !== '') {
|
||||
$target = '/recherche';
|
||||
|
||||
$specSlug = '';
|
||||
if ($specialite === '__OTHERS__') {
|
||||
$specSlug = 'autres-specialistes';
|
||||
} elseif ($specialite !== '') {
|
||||
$specSlug = seo_spec_slug_from_input($specialite) ?? '';
|
||||
}
|
||||
|
||||
if ($specSlug !== '') {
|
||||
$target .= '/' . rawurlencode($specSlug);
|
||||
}
|
||||
|
||||
$citySlug = '';
|
||||
if ($ville !== '') {
|
||||
$citySlug = seo_city_slug_from_input($ville);
|
||||
}
|
||||
|
||||
if ($citySlug !== '') {
|
||||
if ($specSlug === '') {
|
||||
// si ville seule, on garde la query string pour éviter une structure bancale
|
||||
$target = '/recherche/?' . http_build_query([
|
||||
'specialite' => $specialite,
|
||||
'ville' => $ville,
|
||||
]);
|
||||
header('Location: ' . $target, true, 301);
|
||||
exit;
|
||||
}
|
||||
|
||||
$target .= '/' . rawurlencode($citySlug);
|
||||
}
|
||||
|
||||
$target .= '/';
|
||||
|
||||
$extra = $_GET;
|
||||
unset($extra['specialite'], $extra['ville']);
|
||||
|
||||
if (! empty($extra)) {
|
||||
$target .= '?' . http_build_query($extra);
|
||||
}
|
||||
|
||||
header('Location: ' . $target, true, 301);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($UGLY_TO_PRETTY[$path])) {
|
||||
$target = $UGLY_TO_PRETTY[$path];
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
function has_cyrillic(string $s): bool {
|
||||
return (bool) preg_match('/\p{Cyrillic}/u', $s);
|
||||
}
|
||||
|
||||
function norm_key(string $s): string {
|
||||
$s = mb_strtolower(trim($s), 'UTF-8');
|
||||
$s = str_replace(['ё'], ['е'], $s); // homogénéiser RU
|
||||
$s = preg_replace('/[\'"’\-\s]+/u', '', $s); // enlève tirets, espaces, quotes
|
||||
return $s;
|
||||
}
|
||||
|
||||
// Translittération simple RU -> latin (fallback si intl n’est pas dispo)
|
||||
function translit_ru_to_lat(string $s): string {
|
||||
if (class_exists('Transliterator')) {
|
||||
// "Russian-Latin/BGN" produit Parizh (Париж) -> "parizh"
|
||||
$t = \Transliterator::create('Russian-Latin/BGN; Lower();');
|
||||
if ($t) return $t->transliterate($s);
|
||||
}
|
||||
$map = [
|
||||
'а'=>'a','б'=>'b','в'=>'v','г'=>'g','д'=>'d','е'=>'e','ё'=>'e','ж'=>'zh','з'=>'z','и'=>'i','й'=>'i','к'=>'k',
|
||||
'л'=>'l','м'=>'m','н'=>'n','о'=>'o','п'=>'p','р'=>'r','с'=>'s','т'=>'t','у'=>'u','ф'=>'f','х'=>'kh','ц'=>'ts',
|
||||
'ч'=>'ch','ш'=>'sh','щ'=>'shch','ъ'=>'','ы'=>'y','ь'=>'','э'=>'e','ю'=>'yu','я'=>'ya',
|
||||
];
|
||||
$s = mb_strtolower($s, 'UTF-8');
|
||||
return strtr($s, $map);
|
||||
}
|
||||
|
||||
// Retourne une liste de candidats latins (pour LIKE) à partir d’une saisie q
|
||||
function city_query_candidates(string $q): array {
|
||||
$q = trim($q);
|
||||
if ($q === '') return [];
|
||||
|
||||
$cands = [];
|
||||
$cands[] = mb_strtolower($q, 'UTF-8'); // saisie brute (si déjà latine)
|
||||
|
||||
// charger alias RU -> latin
|
||||
$aliases = @include __DIR__ . '/city_aliases_ru.php';
|
||||
if (!is_array($aliases)) $aliases = [];
|
||||
|
||||
if (has_cyrillic($q)) {
|
||||
// 1) correspondance par préfixe dans les alias (ПАР → Paris)
|
||||
$key = norm_key($q);
|
||||
foreach ($aliases as $ru => $lat) {
|
||||
if (strpos(norm_key($ru), $key) === 0) {
|
||||
$cands[] = $lat;
|
||||
}
|
||||
}
|
||||
// 2) translittération simple (Париж -> parizh)
|
||||
$cands[] = translit_ru_to_lat($q);
|
||||
}
|
||||
|
||||
// Normalisation légère des candidats (minuscules, accents retirés si possible)
|
||||
$res = [];
|
||||
foreach (array_unique($cands) as $c) {
|
||||
$lc = mb_strtolower($c, 'UTF-8');
|
||||
// enlever accents (si intl transliterator dispo)
|
||||
if (class_exists('Transliterator')) {
|
||||
$t = \Transliterator::create('Any-Latin; Latin-ASCII;');
|
||||
if ($t) $lc = $t->transliterate($lc);
|
||||
}
|
||||
$res[] = $lc;
|
||||
}
|
||||
return array_values(array_unique($res));
|
||||
}
|
||||
+396
-56
@@ -1,65 +1,405 @@
|
||||
<?php
|
||||
function has_cyrillic(string $s): bool {
|
||||
return (bool) preg_match('/\p{Cyrillic}/u', $s);
|
||||
|
||||
function has_cyrillic(string $s): bool
|
||||
{
|
||||
return (bool) preg_match('/\p{Cyrillic}/u', $s);
|
||||
}
|
||||
|
||||
function norm_key(string $s): string {
|
||||
$s = mb_strtolower(trim($s), 'UTF-8');
|
||||
$s = str_replace(['ё'], ['е'], $s); // homogénéiser RU
|
||||
$s = preg_replace('/[\'"’\-\s]+/u', '', $s); // enlève tirets, espaces, quotes
|
||||
return $s;
|
||||
function norm_key(string $s): string
|
||||
{
|
||||
$s = mb_strtolower(trim($s), 'UTF-8');
|
||||
$s = str_replace(['ё'], ['е'], $s);
|
||||
$s = preg_replace('/[\'"’\-\s]+/u', '', $s);
|
||||
return $s;
|
||||
}
|
||||
|
||||
// Translittération simple RU -> latin (fallback si intl n’est pas dispo)
|
||||
function translit_ru_to_lat(string $s): string {
|
||||
if (class_exists('Transliterator')) {
|
||||
// "Russian-Latin/BGN" produit Parizh (Париж) -> "parizh"
|
||||
$t = \Transliterator::create('Russian-Latin/BGN; Lower();');
|
||||
if ($t) return $t->transliterate($s);
|
||||
}
|
||||
$map = [
|
||||
'а'=>'a','б'=>'b','в'=>'v','г'=>'g','д'=>'d','е'=>'e','ё'=>'e','ж'=>'zh','з'=>'z','и'=>'i','й'=>'i','к'=>'k',
|
||||
'л'=>'l','м'=>'m','н'=>'n','о'=>'o','п'=>'p','р'=>'r','с'=>'s','т'=>'t','у'=>'u','ф'=>'f','х'=>'kh','ц'=>'ts',
|
||||
'ч'=>'ch','ш'=>'sh','щ'=>'shch','ъ'=>'','ы'=>'y','ь'=>'','э'=>'e','ю'=>'yu','я'=>'ya',
|
||||
];
|
||||
$s = mb_strtolower($s, 'UTF-8');
|
||||
return strtr($s, $map);
|
||||
}
|
||||
|
||||
// Retourne une liste de candidats latins (pour LIKE) à partir d’une saisie q
|
||||
function city_query_candidates(string $q): array {
|
||||
$q = trim($q);
|
||||
if ($q === '') return [];
|
||||
|
||||
$cands = [];
|
||||
$cands[] = mb_strtolower($q, 'UTF-8'); // saisie brute (si déjà latine)
|
||||
|
||||
// charger alias RU -> latin
|
||||
$aliases = @include __DIR__ . '/city_aliases_ru.php';
|
||||
if (!is_array($aliases)) $aliases = [];
|
||||
|
||||
if (has_cyrillic($q)) {
|
||||
// 1) correspondance par préfixe dans les alias (ПАР → Paris)
|
||||
$key = norm_key($q);
|
||||
foreach ($aliases as $ru => $lat) {
|
||||
if (strpos(norm_key($ru), $key) === 0) {
|
||||
$cands[] = $lat;
|
||||
}
|
||||
}
|
||||
// 2) translittération simple (Париж -> parizh)
|
||||
$cands[] = translit_ru_to_lat($q);
|
||||
}
|
||||
|
||||
// Normalisation légère des candidats (minuscules, accents retirés si possible)
|
||||
$res = [];
|
||||
foreach (array_unique($cands) as $c) {
|
||||
$lc = mb_strtolower($c, 'UTF-8');
|
||||
// enlever accents (si intl transliterator dispo)
|
||||
function translit_ru_to_lat(string $s): string
|
||||
{
|
||||
if (class_exists('Transliterator')) {
|
||||
$t = \Transliterator::create('Any-Latin; Latin-ASCII;');
|
||||
if ($t) $lc = $t->transliterate($lc);
|
||||
$t = \Transliterator::create('Russian-Latin/BGN; Lower();');
|
||||
if ($t) {
|
||||
return $t->transliterate($s);
|
||||
}
|
||||
}
|
||||
$res[] = $lc;
|
||||
}
|
||||
return array_values(array_unique($res));
|
||||
|
||||
$map = [
|
||||
'а' => 'a', 'б' => 'b', 'в' => 'v', 'г' => 'g', 'д' => 'd',
|
||||
'е' => 'e', 'ё' => 'e', 'ж' => 'zh', 'з' => 'z', 'и' => 'i',
|
||||
'й' => 'i', 'к' => 'k', 'л' => 'l', 'м' => 'm', 'н' => 'n',
|
||||
'о' => 'o', 'п' => 'p', 'р' => 'r', 'с' => 's', 'т' => 't',
|
||||
'у' => 'u', 'ф' => 'f', 'х' => 'kh', 'ц' => 'ts', 'ч' => 'ch',
|
||||
'ш' => 'sh', 'щ' => 'shch', 'ъ' => '', 'ы' => 'y', 'ь' => '',
|
||||
'э' => 'e', 'ю' => 'yu', 'я' => 'ya',
|
||||
];
|
||||
|
||||
$s = mb_strtolower($s, 'UTF-8');
|
||||
return strtr($s, $map);
|
||||
}
|
||||
|
||||
// Retourne une liste de candidats latins (pour LIKE) à partir d’une saisie ville
|
||||
function city_query_candidates(string $q): array
|
||||
{
|
||||
$q = trim($q);
|
||||
if ($q === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$aliases = @include __DIR__ . '/city_aliases_ru.php';
|
||||
if (! is_array($aliases)) {
|
||||
$aliases = [];
|
||||
}
|
||||
|
||||
$cands = [];
|
||||
$qLower = mb_strtolower($q, 'UTF-8');
|
||||
|
||||
// Toujours garder la saisie brute
|
||||
$cands[] = $qLower;
|
||||
|
||||
// Version ASCII/latin normalisée
|
||||
$qAscii = $qLower;
|
||||
if (class_exists('Transliterator')) {
|
||||
$t = \Transliterator::create('Any-Latin; Latin-ASCII; Lower();');
|
||||
if ($t) {
|
||||
$qAscii = $t->transliterate($qAscii);
|
||||
}
|
||||
}
|
||||
$cands[] = $qAscii;
|
||||
|
||||
// Si saisie en cyrillique : on ajoute les villes latines correspondantes
|
||||
if (has_cyrillic($q)) {
|
||||
$key = norm_key($q);
|
||||
|
||||
foreach ($aliases as $ru => $lat) {
|
||||
if (strpos(norm_key($ru), $key) === 0) {
|
||||
$cands[] = mb_strtolower($lat, 'UTF-8');
|
||||
$cands[] = mb_strtolower($ru, 'UTF-8');
|
||||
}
|
||||
}
|
||||
|
||||
$cands[] = translit_ru_to_lat($q);
|
||||
}
|
||||
|
||||
// Si saisie en latin : on cherche aussi les correspondances RU depuis les aliases
|
||||
foreach ($aliases as $ru => $lat) {
|
||||
$latLower = mb_strtolower($lat, 'UTF-8');
|
||||
|
||||
$latAscii = $latLower;
|
||||
if (class_exists('Transliterator')) {
|
||||
$t = \Transliterator::create('Any-Latin; Latin-ASCII; Lower();');
|
||||
if ($t) {
|
||||
$latAscii = $t->transliterate($latAscii);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
strpos($latLower, $qLower) === 0 ||
|
||||
strpos($latAscii, $qAscii) === 0
|
||||
) {
|
||||
$cands[] = mb_strtolower($ru, 'UTF-8');
|
||||
$cands[] = $latLower;
|
||||
}
|
||||
}
|
||||
|
||||
$res = [];
|
||||
foreach (array_unique($cands) as $c) {
|
||||
$res[] = mb_strtolower(trim($c), 'UTF-8');
|
||||
}
|
||||
|
||||
return array_values(array_unique(array_filter($res)));
|
||||
}
|
||||
|
||||
function seo_slug(string $s): string
|
||||
{
|
||||
$s = trim(mb_strtolower($s, 'UTF-8'));
|
||||
|
||||
if ($s === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (has_cyrillic($s)) {
|
||||
$aliases = @include __DIR__ . '/city_aliases_ru.php';
|
||||
if (is_array($aliases)) {
|
||||
foreach ($aliases as $ru => $lat) {
|
||||
if (norm_key($ru) === norm_key($s)) {
|
||||
$s = $lat;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (has_cyrillic($s)) {
|
||||
$s = translit_ru_to_lat($s);
|
||||
}
|
||||
}
|
||||
|
||||
if (class_exists('Transliterator')) {
|
||||
$t = \Transliterator::create('Any-Latin; Latin-ASCII; Lower();');
|
||||
if ($t) {
|
||||
$s = $t->transliterate($s);
|
||||
}
|
||||
}
|
||||
|
||||
$s = str_replace(['œ', 'æ'], ['oe', 'ae'], $s);
|
||||
$s = preg_replace('/[^\p{L}\p{N}]+/u', '-', $s);
|
||||
$s = trim($s, '-');
|
||||
$s = preg_replace('/-+/u', '-', $s);
|
||||
|
||||
return $s;
|
||||
}
|
||||
|
||||
function seo_unslug(string $s): string
|
||||
{
|
||||
$s = trim(mb_strtolower($s, 'UTF-8'));
|
||||
$s = str_replace('-', ' ', $s);
|
||||
$s = preg_replace('/\s+/u', ' ', $s);
|
||||
return trim($s);
|
||||
}
|
||||
|
||||
function seo_spec_map(): array
|
||||
{
|
||||
return [
|
||||
'psychologue' => [
|
||||
'db' => 'психолог',
|
||||
'search' => [
|
||||
'ru' => 'психолог',
|
||||
'fr' => 'psychologue',
|
||||
'en' => 'psychologist',
|
||||
],
|
||||
'inputs' => ['psychologue', 'psychologues', 'психолог', 'психологи'],
|
||||
],
|
||||
|
||||
'manager' => [
|
||||
'db' => 'менеджер',
|
||||
'search' => [
|
||||
'ru' => 'менеджер',
|
||||
'fr' => 'manager',
|
||||
'en' => 'manager',
|
||||
],
|
||||
'inputs' => ['it manager', 'it-manager', 'manager it', 'responsable it', 'it-менеджер', 'ит менеджер'],
|
||||
],
|
||||
|
||||
'construction' => [
|
||||
'db' => 'строитель',
|
||||
'search' => [
|
||||
'ru' => 'строитель',
|
||||
'fr' => 'construction',
|
||||
'en' => 'construction',
|
||||
],
|
||||
'inputs' => [
|
||||
'строительная компания',
|
||||
'строительная фирма',
|
||||
'строительство',
|
||||
'construction',
|
||||
'entreprise de construction',
|
||||
'bâtiment',
|
||||
'btp',
|
||||
],
|
||||
],
|
||||
|
||||
'pompes-funebres' => [
|
||||
'db' => 'похоронное',
|
||||
'search' => [
|
||||
'ru' => 'похоронное',
|
||||
'fr' => 'pompes funebres',
|
||||
'en' => 'funeral services',
|
||||
],
|
||||
'inputs' => [
|
||||
'похоронное агентство',
|
||||
'похоронное бюро',
|
||||
'ритуальные услуги',
|
||||
'ритуальное агентство',
|
||||
'pompes funebres',
|
||||
'pompes-funebres',
|
||||
'services funeraires',
|
||||
'funerailles',
|
||||
],
|
||||
],
|
||||
|
||||
'avocat' => [
|
||||
'db' => 'адвокат',
|
||||
'search' => [
|
||||
'ru' => 'адвокат',
|
||||
'fr' => 'avocat',
|
||||
'en' => 'lawyer',
|
||||
],
|
||||
'inputs' => ['avocat', 'avocats', 'адвокат', 'адвокаты'],
|
||||
],
|
||||
|
||||
'medecin' => [
|
||||
'db' => 'врач',
|
||||
'search' => [
|
||||
'ru' => 'врач',
|
||||
'fr' => 'medecin',
|
||||
'en' => 'doctor',
|
||||
],
|
||||
'inputs' => ['medecin', 'medecins', 'médecin', 'médecins', 'врач', 'врачи', 'Врач - стоматолог', 'врач-стоматолог'],
|
||||
],
|
||||
|
||||
'consultant' => [
|
||||
'db' => 'консультант',
|
||||
'search' => [
|
||||
'ru' => 'консультант',
|
||||
'fr' => 'consultant',
|
||||
'en' => 'consultant',
|
||||
],
|
||||
'inputs' => ['consultant', 'consultants', 'консультант', 'консультанты', 'Консультант'],
|
||||
],
|
||||
|
||||
'professeur' => [
|
||||
'db' => 'преподаватель',
|
||||
'search' => [
|
||||
'ru' => 'преподаватель',
|
||||
'fr' => 'professeur',
|
||||
'en' => 'teacher',
|
||||
],
|
||||
'inputs' => ['professeur', 'professeurs', 'pedagogue', 'pedagogues', 'преподаватель', 'преподаватели', 'педагог', 'педагоги'],
|
||||
],
|
||||
|
||||
'comptable' => [
|
||||
'db' => 'бухгалтер',
|
||||
'search' => [
|
||||
'ru' => 'бухгалтер',
|
||||
'fr' => 'comptable',
|
||||
'en' => 'accountant',
|
||||
],
|
||||
'inputs' => ['comptable', 'comptables', 'бухгалтер', 'бухгалтеры'],
|
||||
],
|
||||
|
||||
'traducteur' => [
|
||||
'db' => 'переводчик',
|
||||
'search' => [
|
||||
'ru' => 'переводчик',
|
||||
'fr' => 'traducteur',
|
||||
'en' => 'translator',
|
||||
],
|
||||
'inputs' => ['traducteur', 'traducteurs', 'переводчик', 'переводчики'],
|
||||
],
|
||||
|
||||
'notaire' => [
|
||||
'db' => 'нотариус',
|
||||
'search' => [
|
||||
'ru' => 'нотариус',
|
||||
'fr' => 'notaire',
|
||||
'en' => 'notary',
|
||||
],
|
||||
'inputs' => ['notaire', 'notaires', 'нотариус', 'нотариусы'],
|
||||
],
|
||||
|
||||
'guide' => [
|
||||
'db' => 'гид',
|
||||
'search' => [
|
||||
'ru' => 'гид',
|
||||
'fr' => 'guide',
|
||||
'en' => 'guide',
|
||||
],
|
||||
'inputs' => ['guide', 'guides', 'гид', 'гиды'],
|
||||
],
|
||||
|
||||
'agent-immobilier' => [
|
||||
'db' => 'риелтор',
|
||||
'search' => [
|
||||
'ru' => 'риелтор',
|
||||
'fr' => 'agent immobilier',
|
||||
'en' => 'real estate agent',
|
||||
],
|
||||
'inputs' => [
|
||||
'Agent immobilier',
|
||||
'agent immobilier',
|
||||
'agent-immobilier',
|
||||
'agents immobiliers',
|
||||
'agents-immobiliers',
|
||||
'риелтор',
|
||||
'риелторы',
|
||||
],
|
||||
],
|
||||
|
||||
'autres-specialistes' => [
|
||||
'db' => '__OTHERS__',
|
||||
'inputs' => ['autres specialistes', 'autres-specialistes', 'другие', 'другие специалисты'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
function seo_spec_slug_from_input(string $input): ?string
|
||||
{
|
||||
$inputSlug = seo_slug($input);
|
||||
|
||||
foreach (seo_spec_map() as $slug => $cfg) {
|
||||
foreach (($cfg['inputs'] ?? []) as $candidate) {
|
||||
if ($inputSlug === seo_slug((string) $candidate)) {
|
||||
return $slug;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function seo_spec_db_from_slug(string $slug): ?string
|
||||
{
|
||||
$slug = seo_slug($slug);
|
||||
$map = seo_spec_map();
|
||||
|
||||
return $map[$slug]['db'] ?? null;
|
||||
}
|
||||
|
||||
function seo_city_slug_from_input(string $input): string
|
||||
{
|
||||
$input = trim($input);
|
||||
if ($input === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$aliases = @include __DIR__ . '/city_aliases_ru.php';
|
||||
if (! is_array($aliases)) {
|
||||
$aliases = [];
|
||||
}
|
||||
|
||||
$key = norm_key($input);
|
||||
|
||||
foreach ($aliases as $ru => $lat) {
|
||||
if (norm_key($ru) === $key) {
|
||||
return seo_slug((string) $lat);
|
||||
}
|
||||
}
|
||||
|
||||
return seo_slug($input);
|
||||
}
|
||||
|
||||
function seo_spec_slug_from_key(string $key): ?string
|
||||
{
|
||||
$map = [
|
||||
'avocats' => 'avocat',
|
||||
'consultants' => 'consultant',
|
||||
'vrachi' => 'medecin',
|
||||
'psychologues' => 'psychologue',
|
||||
'repetitori' => 'professeur',
|
||||
'finance' => 'comptable',
|
||||
'perevod' => 'traducteur',
|
||||
'notarius' => 'notaire',
|
||||
'gid' => 'guide',
|
||||
'rieltori' => 'agent-immobilier',
|
||||
'drugie' => 'autres-specialistes',
|
||||
];
|
||||
|
||||
return $map[$key] ?? null;
|
||||
}
|
||||
function seo_spec_search_value_from_slug(string $slug, string $lang): ?string
|
||||
{
|
||||
$slug = seo_slug($slug);
|
||||
$lang = mb_strtolower(trim($lang), 'UTF-8');
|
||||
|
||||
$map = seo_spec_map();
|
||||
if (! isset($map[$slug])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$cfg = $map[$slug];
|
||||
|
||||
if (isset($cfg['search'][$lang]) && trim((string) $cfg['search'][$lang]) !== '') {
|
||||
return trim((string) $cfg['search'][$lang]);
|
||||
}
|
||||
|
||||
return $cfg['db'] ?? null;
|
||||
}
|
||||
+24
-25
@@ -1,30 +1,29 @@
|
||||
<?php
|
||||
// Map des clés de traduction -> valeur canonique (singulier) pour la BDD
|
||||
return [
|
||||
'ru' => [
|
||||
'avocats' => 'адвокат',
|
||||
'consultants' => 'консультант',
|
||||
'vrachi' => 'врач',
|
||||
'psychologues' => 'психолог',
|
||||
'repetitori' => 'преподаватель',
|
||||
'finance' => 'бухгалтер', // ajuste si besoin
|
||||
'perevod' => 'переводчик',
|
||||
'notarius' => 'нотариус',
|
||||
'gid' => 'гид',
|
||||
'rieltori' => 'риелтор',
|
||||
|
||||
'ru' => [
|
||||
'avocats' => 'адвокат',
|
||||
'consultants' => 'консультант',
|
||||
'vrachi' => 'врач',
|
||||
'psychologues' => 'психолог',
|
||||
'repetitori' => 'преподаватель',
|
||||
'finance' => 'бухгалтер', // ajuste si besoin
|
||||
'perevod' => 'переводчик',
|
||||
'notarius' => 'нотариус',
|
||||
'gid' => 'гид',
|
||||
'rieltori' => 'риелтор',
|
||||
|
||||
],
|
||||
'fr' => [
|
||||
'avocats' => 'адвокат',
|
||||
'consultants' => 'консультант',
|
||||
'vrachi' => 'врач', // clé existante "vrachi" côté traductions
|
||||
'psychologues' => 'психолог',
|
||||
'repetitori' => 'преподаватель', // ou "Répetiteur" selon ta BDD
|
||||
'finance' => 'бухгалтер', // ajuste si besoin
|
||||
'perevod' => 'переводчик',
|
||||
'notarius' => 'нотариус',
|
||||
'gid' => 'гид',
|
||||
'rieltori' => 'риелтор',
|
||||
],
|
||||
],
|
||||
'fr' => [
|
||||
'avocats' => 'адвокат',
|
||||
'consultants' => 'консультант',
|
||||
'vrachi' => 'врач', // clé existante "vrachi" côté traductions
|
||||
'psychologues' => 'психолог',
|
||||
'repetitori' => 'преподаватель', // ou "Répetiteur" selon ta BDD
|
||||
'finance' => 'бухгалтер', // ajuste si besoin
|
||||
'perevod' => 'переводчик',
|
||||
'notarius' => 'нотариус',
|
||||
'gid' => 'гид',
|
||||
'rieltori' => 'риелтор',
|
||||
],
|
||||
];
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
require_once __DIR__ . '/../includes/bootstrap_global.php';
|
||||
require_once APP_HTDOCS . '/includes/avatar_initiales.php';
|
||||
require_once __DIR__ . '/includes/search_helpers.php';
|
||||
require_once __DIR__ . '/traitement_form_index.php';
|
||||
|
||||
$SPEC_CANON = require __DIR__ . '/includes/spec_map.php';
|
||||
$canon = $SPEC_CANON[$lang] ?? [];
|
||||
@@ -238,35 +240,48 @@ else: ?>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<h1 class="hero-title"><?php echo($translations['titre_home'] ?? '') ?></h1>
|
||||
<h1 class="hero-title">
|
||||
<?php echo($translations['titre_home'] ?? '') ?>
|
||||
<span class="visually-hidden">
|
||||
<?php echo h($translations['boostseo_home_h1_suffix'] ?? '') ?>
|
||||
</span>
|
||||
</h1>
|
||||
<p class="visually-hidden">
|
||||
<?php echo h($translations['boostseo_home_intro'] ?? '') ?>
|
||||
</p>
|
||||
<center><h5><?php echo($translations['sous_titre_home'] ?? '') ?></h5></center>
|
||||
<br>
|
||||
<?php
|
||||
// Chips -> liens (SEO + comme image)
|
||||
$chipKeysRow1 = ['avocats', 'notarius', 'rieltori'];
|
||||
$chipKeysRow2 = ['vrachi', 'psychologues', 'consultants'];
|
||||
$chipKeysRow3 = ['perevod', 'repetitori', 'drugie'];
|
||||
|
||||
$renderChipLinks = function (array $keys) use ($translations, $canon) {
|
||||
$renderChipLinks = function (array $keys) use ($translations) {
|
||||
foreach ($keys as $k) {
|
||||
$label = $translations[$k] ?? $k;
|
||||
$query = ($k === 'drugie') ? '__OTHERS__' : ($canon[$k] ?? $label);
|
||||
$slug = seo_spec_slug_from_key($k);
|
||||
|
||||
$href = '/result_search.php?' . http_build_query([
|
||||
'specialite' => $query,
|
||||
'ville' => '',
|
||||
]);
|
||||
$href = '/recherche/';
|
||||
if ($slug !== null && $slug !== '') {
|
||||
$href .= rawurlencode($slug) . '/';
|
||||
}
|
||||
|
||||
echo '<a class="chip chip-link" href="' . h($href) . '">' . h($label) . '</a>';
|
||||
}
|
||||
};
|
||||
?>
|
||||
<!--<h2 class="home-h2"><?php echo h($translations['home_popular_specialists'] ?? '') ?></h2>-->
|
||||
<h2 class="visually-hidden">
|
||||
<?php echo h($translations['boostseo_home_h2_categories'] ?? '') ?>
|
||||
</h2>
|
||||
<div class="row"><?php $renderChipLinks($chipKeysRow1); ?></div>
|
||||
<div class="row"><?php $renderChipLinks($chipKeysRow2); ?></div>
|
||||
<div class="row"><?php $renderChipLinks($chipKeysRow3); ?></div>
|
||||
|
||||
<br>
|
||||
<h2 class="visually-hidden">
|
||||
<?php echo h($translations['boostseo_home_h2_recent'] ?? '') ?>
|
||||
</h2>
|
||||
<div class="hp-list">
|
||||
<?php
|
||||
try {
|
||||
@@ -420,24 +435,31 @@ else: ?>
|
||||
?>
|
||||
</div>
|
||||
<br><br>
|
||||
<form class="searchbar" action="result_search.php" method="get" id="searchForm">
|
||||
<form class="searchbar" action="/recherche" method="get" id="searchForm">
|
||||
<div class="search-icon">🔍</div>
|
||||
|
||||
<div class="input-wrap">
|
||||
<input type="text" id="specialite" name="specialite" autocomplete="off"
|
||||
placeholder="<?php echo h($translations['n_special'] ?? '') ?>" aria-label="Специальность">
|
||||
<input type="text"
|
||||
id="specialite"
|
||||
name="specialite"
|
||||
autocomplete="off"
|
||||
placeholder="<?php echo h($translations['n_special'] ?? '') ?>"
|
||||
aria-label="Специальность">
|
||||
<ul class="suggest" id="sg-specialite" hidden></ul>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<div class="input-wrap">
|
||||
<input type="text" id="ville" name="ville" autocomplete="off"
|
||||
placeholder="<?php echo h($translations['gde'] ?? '') ?>" aria-label="Город">
|
||||
<input type="text"
|
||||
id="ville"
|
||||
name="ville"
|
||||
autocomplete="off"
|
||||
placeholder="<?php echo h($translations['gde'] ?? '') ?>"
|
||||
aria-label="Город">
|
||||
<ul class="suggest" id="sg-ville" hidden></ul>
|
||||
</div>
|
||||
|
||||
<!-- ✅ bouton inside -->
|
||||
<button class="hp-btn btn-find--inbar" type="submit">
|
||||
<?php echo h($translations['naiti'] ?? '') ?>
|
||||
</button>
|
||||
@@ -445,7 +467,7 @@ else: ?>
|
||||
<div class="spec-cta-wrap">
|
||||
<h3><?php echo h($translations['choix_pro_titre_home'] ?? '') ?></h3>
|
||||
<p><!--<?php echo h($translations['choix_pro_texte_court'] ?? '') ?>--></p>
|
||||
<a class="spec-cta" href="/inscription_praticien.php">
|
||||
<a class="spec-cta" href="/registration.php">
|
||||
<span class="spec-cta__label"><?php echo h($translations['titre_spec'] ?? '') ?></span>
|
||||
</a>
|
||||
</div>
|
||||
@@ -457,222 +479,9 @@ else: ?>
|
||||
</div>-->
|
||||
<br>
|
||||
|
||||
<!-- Articles -->
|
||||
<!--<h2 class="home-h2"><?php echo h($translations['home_useful_articles'] ?? '') ?></h2>-->
|
||||
|
||||
<?php
|
||||
function fetch_url(string $url, int $timeout = 4): array
|
||||
{
|
||||
// Retourne [ok(bool), body(string), err(string), http(int)]
|
||||
if (! function_exists('curl_init')) {
|
||||
return [false, '', 'cURL non disponible', 0];
|
||||
}
|
||||
<?php include __DIR__ . '/form_index.php'; ?>
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_CONNECTTIMEOUT => $timeout,
|
||||
CURLOPT_TIMEOUT => $timeout,
|
||||
CURLOPT_USERAGENT => 'uslugi.fr (+https://staging.uslugi.fr)',
|
||||
CURLOPT_SSL_VERIFYPEER => true,
|
||||
CURLOPT_SSL_VERIFYHOST => 2,
|
||||
]);
|
||||
|
||||
$body = (string) curl_exec($ch);
|
||||
$err = curl_error($ch);
|
||||
$http = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
$ok = ($err === '' && $http >= 200 && $http < 300 && $body !== '');
|
||||
return [$ok, $body, $err, $http];
|
||||
}
|
||||
function decode_entities(string $s): string
|
||||
{
|
||||
// ENT_HTML5 gère correctement les entités modernes
|
||||
return html_entity_decode($s, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
}
|
||||
|
||||
function fetch_sovetnik_articles(int $limit = 4, bool $debug = false): array
|
||||
{
|
||||
$cacheFile = APP_HTDOCS . '/cache/sovetnik_feed.json';
|
||||
$lockFile = APP_HTDOCS . '/cache/sovetnik_feed.lock';
|
||||
|
||||
// Soft TTL: on essaye de rafraîchir après 15 min
|
||||
// Hard TTL: on accepte d'afficher un cache jusqu'à 24h (pour ne jamais bloquer la page)
|
||||
$ttlSoft = 15 * 60;
|
||||
$ttlHard = 24 * 60 * 60;
|
||||
|
||||
$now = time();
|
||||
$hasCache = is_file($cacheFile);
|
||||
$age = $hasCache ? ($now - filemtime($cacheFile)) : PHP_INT_MAX;
|
||||
|
||||
// Helper pour lire cache
|
||||
$readCache = function () use ($cacheFile, $limit): array {
|
||||
$data = json_decode((string) @file_get_contents($cacheFile), true);
|
||||
return (is_array($data) && $data) ? array_slice($data, 0, $limit) : [];
|
||||
};
|
||||
|
||||
// 1) Cache frais => retour immédiat (rapide)
|
||||
if ($hasCache && $age < $ttlSoft) {
|
||||
return $readCache();
|
||||
}
|
||||
|
||||
// 2) Cache expiré mais pas trop vieux => on le garde en "stale" (fallback instantané)
|
||||
$stale = [];
|
||||
if ($hasCache && $age < $ttlHard) {
|
||||
$stale = $readCache();
|
||||
}
|
||||
|
||||
// 3) On tente un refresh, MAIS sans bloquer longtemps
|
||||
// + lock pour éviter que plusieurs requêtes refreshent en même temps
|
||||
$canRefresh = true;
|
||||
|
||||
if (is_file($lockFile)) {
|
||||
// lock encore récent => on ne refresh pas
|
||||
if (($now - filemtime($lockFile)) < 60) {
|
||||
$canRefresh = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (! $canRefresh) {
|
||||
// On sert le stale cache (ou vide)
|
||||
return $stale;
|
||||
}
|
||||
|
||||
// On pose le lock (best-effort)
|
||||
@is_dir(dirname($lockFile)) || @mkdir(dirname($lockFile), 0755, true);
|
||||
@file_put_contents($lockFile, (string) $now);
|
||||
|
||||
// Timeout court = clé de la perf
|
||||
$timeout = 1;
|
||||
|
||||
// 1) RSS
|
||||
$feedCandidates = [
|
||||
'https://www.sovetnik.fr/feed/',
|
||||
'https://www.sovetnik.fr/?feed=rss2',
|
||||
'https://www.sovetnik.fr/feed/rss2/',
|
||||
];
|
||||
|
||||
foreach ($feedCandidates as $feedUrl) {
|
||||
[$ok, $xmlStr, $err, $http] = fetch_url($feedUrl, $timeout);
|
||||
|
||||
if ($debug) {
|
||||
echo "\n<!-- Sovetnik RSS try: $feedUrl | ok=" . ($ok ? '1' : '0') . " | http=$http | err=" . htmlspecialchars($err) . " -->\n";
|
||||
}
|
||||
|
||||
if (! $ok) {
|
||||
continue;
|
||||
}
|
||||
|
||||
libxml_use_internal_errors(true);
|
||||
$xml = simplexml_load_string($xmlStr);
|
||||
if (! $xml || empty($xml->channel->item)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$out = [];
|
||||
foreach ($xml->channel->item as $item) {
|
||||
$titleRaw = (string) $item->title;
|
||||
$descRaw = (string) $item->description;
|
||||
|
||||
$out[] = [
|
||||
'url' => (string) $item->link,
|
||||
'title' => trim(decode_entities(strip_tags($titleRaw))),
|
||||
'desc' => trim(decode_entities(strip_tags($descRaw))),
|
||||
'img' => '',
|
||||
];
|
||||
|
||||
if (count($out) >= $limit) {
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if ($out) {
|
||||
@is_dir(dirname($cacheFile)) || @mkdir(dirname($cacheFile), 0755, true);
|
||||
@file_put_contents($cacheFile, json_encode($out, JSON_UNESCAPED_UNICODE));
|
||||
@unlink($lockFile);
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Fallback API
|
||||
$apiUrl = 'https://www.sovetnik.fr/wp-json/wp/v2/posts?per_page=' . (int) $limit . '&_embed=1';
|
||||
[$ok, $jsonStr, $err, $http] = fetch_url($apiUrl, $timeout);
|
||||
|
||||
if ($debug) {
|
||||
echo "\n<!-- Sovetnik API try: $apiUrl | ok=" . ($ok ? '1' : '0') . " | http=$http | err=" . htmlspecialchars($err) . " -->\n";
|
||||
}
|
||||
|
||||
if ($ok) {
|
||||
$arr = json_decode($jsonStr, true);
|
||||
if (is_array($arr)) {
|
||||
$out = [];
|
||||
foreach ($arr as $p) {
|
||||
$title = trim(decode_entities(strip_tags($p['title']['rendered'] ?? '')));
|
||||
$desc = trim(decode_entities(strip_tags($p['excerpt']['rendered'] ?? '')));
|
||||
|
||||
$img = '';
|
||||
if (! empty($p['_embedded']['wp:featuredmedia'][0]['source_url'])) {
|
||||
$img = (string) $p['_embedded']['wp:featuredmedia'][0]['source_url'];
|
||||
}
|
||||
|
||||
$out[] = [
|
||||
'url' => (string) ($p['link'] ?? ''),
|
||||
'title' => $title,
|
||||
'desc' => $desc,
|
||||
'img' => $img,
|
||||
];
|
||||
|
||||
if (count($out) >= $limit) {
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if ($out) {
|
||||
@is_dir(dirname($cacheFile)) || @mkdir(dirname($cacheFile), 0755, true);
|
||||
@file_put_contents($cacheFile, json_encode($out, JSON_UNESCAPED_UNICODE));
|
||||
@unlink($lockFile);
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Échec refresh => on enlève le lock et on sert le stale
|
||||
@unlink($lockFile);
|
||||
return $stale;
|
||||
}
|
||||
|
||||
// Debug optionnel
|
||||
$debugArticles = isset($_GET['debug']) && $_GET['debug'] === '1';
|
||||
$articles = fetch_sovetnik_articles(4, $debugArticles);
|
||||
?>
|
||||
|
||||
|
||||
|
||||
<div class="hp-articles">
|
||||
<?php if (! empty($articles)): ?>
|
||||
<?php foreach ($articles as $a): ?>
|
||||
<a class="ha" href="<?php echo h($a['url']) ?>" target="_blank" rel="noopener">
|
||||
<div class="ha-img" style="<?php echo ! empty($a['img']) ? 'background:url(' . h($a['img']) . ') top center/cover no-repeat;' : '' ?>"></div>
|
||||
<div class="ha-body">
|
||||
<p class="ha-title"><?php echo h($a['title']) ?></p>
|
||||
<p class="ha-sub"><?php echo h(mb_strimwidth($a['desc'], 0, 120, '…', 'UTF-8')) ?></p>
|
||||
</div>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<div class="text-muted"><?php echo h($translations['home_articles_unavailable'] ?? 'Articles indisponibles pour le moment.') ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!--<div class="hp-all">
|
||||
<a href="https://www.sovetnik.fr/" target="_blank" rel="noopener">
|
||||
<?php echo h($translations['home_all_articles'] ?? '') ?>
|
||||
</a>
|
||||
</div>-->
|
||||
|
||||
<br>
|
||||
</div>
|
||||
@@ -680,7 +489,10 @@ else: ?>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<div class="visually-hidden">
|
||||
<h2><?php echo h($translations['boostseo_home_h2_cities'] ?? '') ?></h2>
|
||||
<p><?php echo h($translations['boostseo_home_cities_text'] ?? '') ?></p>
|
||||
</div>
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p><?php echo $translations['copyr'] ?? '' ?><a href="/terms_fr.php" target="_blank"></a></p>
|
||||
|
||||
@@ -0,0 +1,722 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
|
||||
require_once __DIR__ . '/../includes/bootstrap_global.php';
|
||||
require_once APP_HTDOCS . '/includes/avatar_initiales.php';
|
||||
require_once __DIR__ . '/includes/search_helpers.php';
|
||||
|
||||
$SPEC_CANON = require __DIR__ . '/includes/spec_map.php';
|
||||
$canon = $SPEC_CANON[$lang] ?? [];
|
||||
|
||||
/* =========================
|
||||
AJAX: suggestions
|
||||
========================= */
|
||||
if (isset($_GET['ajax']) && $_GET['ajax'] === 'suggest') {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$field = (string) ($_GET['field'] ?? '');
|
||||
$q = trim((string) ($_GET['q'] ?? ''));
|
||||
if (! in_array($field, ['specialite', 'ville'], true)) {echo json_encode([]);exit;}
|
||||
|
||||
require_once __DIR__ . '/includes/search_helpers.php';
|
||||
|
||||
$params = [];
|
||||
$sql = '';
|
||||
|
||||
if ($field === 'ville') {
|
||||
$sql = "SELECT DISTINCT ville AS val
|
||||
FROM praticien
|
||||
WHERE ville IS NOT NULL AND ville <> ''";
|
||||
|
||||
if ($q !== '') {
|
||||
$cands = city_query_candidates($q);
|
||||
$cands[] = mb_strtolower($q, 'UTF-8');
|
||||
$cands = array_values(array_unique($cands));
|
||||
|
||||
$ors = [];
|
||||
foreach ($cands as $i => $cand) {
|
||||
$ors[] = "LOWER(ville) LIKE :q$i";
|
||||
$params[":q$i"] = $cand . '%';
|
||||
}
|
||||
if ($ors) {
|
||||
$sql .= " AND (" . implode(' OR ', $ors) . ")";
|
||||
}
|
||||
|
||||
}
|
||||
$sql .= " ORDER BY val ASC LIMIT 15";
|
||||
|
||||
} else {
|
||||
$sql = "SELECT DISTINCT val FROM (
|
||||
SELECT specialite AS val
|
||||
FROM praticien
|
||||
WHERE specialite IS NOT NULL AND specialite <> ''
|
||||
UNION ALL
|
||||
SELECT CONCAT(prenom, ' ', nom) AS val
|
||||
FROM praticien
|
||||
WHERE prenom IS NOT NULL AND prenom <> '' AND nom IS NOT NULL AND nom <> ''
|
||||
UNION ALL
|
||||
SELECT prenom AS val
|
||||
FROM praticien
|
||||
WHERE prenom IS NOT NULL AND prenom <> ''
|
||||
UNION ALL
|
||||
SELECT nom AS val
|
||||
FROM praticien
|
||||
WHERE nom IS NOT NULL AND nom <> ''
|
||||
) t
|
||||
WHERE 1";
|
||||
|
||||
if ($q !== '') {
|
||||
$sql .= " AND LOWER(val) LIKE :q";
|
||||
$params[':q'] = mb_strtolower($q, 'UTF-8') . '%';
|
||||
}
|
||||
|
||||
$sql .= " ORDER BY (LOWER(val) LIKE :qprefix) DESC, val ASC LIMIT 15";
|
||||
$params[':qprefix'] = ($q !== '') ? (mb_strtolower($q, 'UTF-8') . '%') : '';
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
echo json_encode($stmt->fetchAll(PDO::FETCH_COLUMN));
|
||||
exit;
|
||||
}
|
||||
|
||||
/* =========================
|
||||
Helpers
|
||||
========================= */
|
||||
function h($v): string
|
||||
{return htmlspecialchars((string) $v, ENT_QUOTES, 'UTF-8');}
|
||||
$reqUri = h($_SERVER['REQUEST_URI'] ?? '/');
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="<?php echo h($lang ?? 'ru') ?>">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<title><?php echo h($translations['seo_index_title'] ?? '') ?></title>
|
||||
<meta name="description" content="<?php echo h($translations['seo_index_desc'] ?? '') ?>">
|
||||
<meta name="keywords" content="<?php echo h($translations['seo_index_keywords'] ?? '') ?>">
|
||||
<meta name="author" content="Uslugi.fr">
|
||||
<meta name="language" content="<?php echo h($lang ?? 'ru') ?>">
|
||||
|
||||
<!-- ✅ Canonical + Hreflang (SEO multilingue) -->
|
||||
<link rel="canonical" href="https://uslugi.fr<?php echo $reqUri ?>">
|
||||
<link rel="alternate" hreflang="ru" href="https://uslugi.fr/ru/">
|
||||
<link rel="alternate" hreflang="fr" href="https://uslugi.fr/fr/">
|
||||
<link rel="alternate" hreflang="x-default" href="https://uslugi.fr/">
|
||||
|
||||
<!-- ✅ Open Graph -->
|
||||
<meta property="og:title" content="<?php echo h($translations['seo_index_title'] ?? '') ?>">
|
||||
<meta property="og:description" content="<?php echo h($translations['seo_index_desc'] ?? '') ?>">
|
||||
<meta property="og:image" content="https://uslugi.fr/public/uploads/img/logo_u.png">
|
||||
<meta property="og:url" content="https://uslugi.fr<?php echo $reqUri ?>">
|
||||
<meta property="og:type" content="website">
|
||||
|
||||
<!-- PWA -->
|
||||
<link rel="manifest" href="/manifest.webmanifest">
|
||||
<meta name="theme-color" content="#38378B">
|
||||
|
||||
<!-- Favicons -->
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/public/uploads/img/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/public/uploads/img/favicon-16x16.png">
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="/public/uploads/img/favicon-96x96.png">
|
||||
<link rel="icon" href="/public/uploads/img/favicon.ico" type="image/x-icon">
|
||||
<link rel="icon" type="image/svg+xml" href="/public/uploads/img/favicon.svg">
|
||||
<meta name="apple-mobile-web-app-title" content="Uslugi.fr">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/public/uploads/img/apple-touch-icon.png">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
||||
|
||||
<!-- DNS Prefetch -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="preconnect" href="https://cdn.jsdelivr.net">
|
||||
<link rel="dns-prefetch" href="//fonts.googleapis.com">
|
||||
|
||||
<!-- CSS -->
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
<?php if (! empty($logged)): ?>
|
||||
<link rel="stylesheet" href="<?php echo h(asset('/includes/site_style.css')) ?>">
|
||||
<?php endif; ?>
|
||||
<link rel="stylesheet" href="<?php echo h(asset('/includes/custom_style.css')) ?>">
|
||||
<link rel="stylesheet" href="<?php echo h(asset('/includes/index_style.css')) ?>">
|
||||
<link rel="stylesheet" href="<?php echo h(asset('/includes/mobil_style.css')) ?>">
|
||||
<?php include_once __DIR__ . '/includes/tracking.php'; ?>
|
||||
<style>
|
||||
.site-header
|
||||
{
|
||||
background: #FFF;
|
||||
border-bottom: 0px solid rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
.site-footer
|
||||
{
|
||||
background: #FFF;
|
||||
border-top: 0px solid rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
</style>
|
||||
<!-- ✅ Schema (simple, tu peux enrichir plus tard) -->
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context":"https://schema.org",
|
||||
"@type":"LocalBusiness",
|
||||
"name":"Uslugi.fr",
|
||||
"url":"https://uslugi.fr",
|
||||
"logo":"https://uslugi.fr/public/uploads/img/logo.png",
|
||||
"address":{"@type":"PostalAddress","addressCountry":"FR"}
|
||||
}
|
||||
</script>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<?php if (! empty($logged)):
|
||||
include_header_clean(APP_HTDOCS . '/public/includes/header.php');
|
||||
else: ?>
|
||||
<!-- Header public identique -->
|
||||
<header class="site-header">
|
||||
<div class="container">
|
||||
<div class="header-left">
|
||||
<div class="brand">
|
||||
<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">
|
||||
</div>
|
||||
<div class="lang-switch">
|
||||
<?php require __DIR__ . '/includes/menu_langue.php'; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="header-actions">
|
||||
<a class="btn-login" href="/connexion.php">
|
||||
<span class="d-sm-none"><?php echo h($translations['btn_se_connecter'] ?? '') ?></span>
|
||||
<span class="d-none d-sm-inline"><?php echo h($translations['ja_spec'] ?? '') ?></span>
|
||||
</a>
|
||||
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-logo rounded-circle d-inline-flex align-items-center justify-content-center question-btn"
|
||||
type="button" data-bs-toggle="dropdown" data-bs-auto-close="true"
|
||||
aria-expanded="false" aria-label="<?php echo h($translations['help'] ?? 'Помощь') ?>">?</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end shadow help-menu menu-panel">
|
||||
<li><a href="/how-it-work.php"><?php echo h($translations['kak_rabotaet'] ?? '') ?></a></li>
|
||||
<li><a href="/about.php"><?php echo h($translations['pro_nas'] ?? '') ?></a></li>
|
||||
<li><a href="/faq.php"><?php echo h($translations['faq'] ?? '') ?></a></li>
|
||||
<li><a href="/contact.php"><?php echo h($translations['nam_pisat'] ?? '') ?></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- HERO -->
|
||||
<section class="hero">
|
||||
<div class="home-panel" role="region" aria-label="<?php echo h($translations['home_panel_aria'] ?? 'Accueil') ?>">
|
||||
<div class="container hero-inner">
|
||||
|
||||
<!-- PWA Install floating button (mobile only, hidden by default) ✅ -->
|
||||
<button id="pwaInstallBtn" class="pwa-install d-none" type="button" aria-label="Installer USLUGI">
|
||||
⤓ Uslugi
|
||||
</button>
|
||||
|
||||
<!-- iOS helper (hidden by default) ✅ -->
|
||||
<div id="pwaIosHint" class="pwa-ios-hint d-none" role="dialog" aria-modal="true">
|
||||
<div class="pwa-ios-card">
|
||||
<div class="pwa-ios-title">USLUGI.FR</div>
|
||||
<div class="pwa-ios-text">
|
||||
На iPhone: нажмите (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-box-arrow-up" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M3.5 6a.5.5 0 0 0-.5.5v8a.5.5 0 0 0 .5.5h9a.5.5 0 0 0 .5-.5v-8a.5.5 0 0 0-.5-.5h-2a.5.5 0 0 1 0-1h2A1.5 1.5 0 0 1 14 6.5v8a1.5 1.5 0 0 1-1.5 1.5h-9A1.5 1.5 0 0 1 2 14.5v-8A1.5 1.5 0 0 1 3.5 5h2a.5.5 0 0 1 0 1z"/>
|
||||
<path fill-rule="evenodd" d="M7.646.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 1.707V10.5a.5.5 0 0 1-1 0V1.707L5.354 3.854a.5.5 0 1 1-.708-.708z"/>
|
||||
</svg>) <b>Поделиться,</b> затем <b>Добавить на экран</b>.<br><br>
|
||||
Sur iPhone : appuie sur <b>Partager</b> (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-box-arrow-up" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M3.5 6a.5.5 0 0 0-.5.5v8a.5.5 0 0 0 .5.5h9a.5.5 0 0 0 .5-.5v-8a.5.5 0 0 0-.5-.5h-2a.5.5 0 0 1 0-1h2A1.5 1.5 0 0 1 14 6.5v8a1.5 1.5 0 0 1-1.5 1.5h-9A1.5 1.5 0 0 1 2 14.5v-8A1.5 1.5 0 0 1 3.5 5h2a.5.5 0 0 1 0 1z"/>
|
||||
<path fill-rule="evenodd" d="M7.646.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 1.707V10.5a.5.5 0 0 1-1 0V1.707L5.354 3.854a.5.5 0 1 1-.708-.708z"/>
|
||||
</svg>), puis <b>Sur l'écran d'accueil</b>.
|
||||
</div>
|
||||
<button id="pwaIosClose" class="pwa-ios-close" type="button">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<h1 class="hero-title">
|
||||
<?php echo($translations['titre_home'] ?? '') ?>
|
||||
<span class="visually-hidden">
|
||||
<?php echo h($translations['boostseo_home_h1_suffix'] ?? '') ?>
|
||||
</span>
|
||||
</h1>
|
||||
<p class="visually-hidden">
|
||||
<?php echo h($translations['boostseo_home_intro'] ?? '') ?>
|
||||
</p>
|
||||
<center><h5><?php echo($translations['sous_titre_home'] ?? '') ?></h5></center>
|
||||
<br>
|
||||
<?php
|
||||
$chipKeysRow1 = ['avocats', 'notarius', 'rieltori'];
|
||||
$chipKeysRow2 = ['vrachi', 'psychologues', 'consultants'];
|
||||
$chipKeysRow3 = ['perevod', 'repetitori', 'drugie'];
|
||||
|
||||
$renderChipLinks = function (array $keys) use ($translations) {
|
||||
foreach ($keys as $k) {
|
||||
$label = $translations[$k] ?? $k;
|
||||
$slug = seo_spec_slug_from_key($k);
|
||||
|
||||
$href = '/recherche/';
|
||||
if ($slug !== null && $slug !== '') {
|
||||
$href .= rawurlencode($slug) . '/';
|
||||
}
|
||||
|
||||
echo '<a class="chip chip-link" href="' . h($href) . '">' . h($label) . '</a>';
|
||||
}
|
||||
};
|
||||
?>
|
||||
<!--<h2 class="home-h2"><?php echo h($translations['home_popular_specialists'] ?? '') ?></h2>-->
|
||||
<h2 class="visually-hidden">
|
||||
<?php echo h($translations['boostseo_home_h2_categories'] ?? '') ?>
|
||||
</h2>
|
||||
<div class="row"><?php $renderChipLinks($chipKeysRow1); ?></div>
|
||||
<div class="row"><?php $renderChipLinks($chipKeysRow2); ?></div>
|
||||
<div class="row"><?php $renderChipLinks($chipKeysRow3); ?></div>
|
||||
|
||||
<br>
|
||||
<h2 class="visually-hidden">
|
||||
<?php echo h($translations['boostseo_home_h2_recent'] ?? '') ?>
|
||||
</h2>
|
||||
<div class="hp-list">
|
||||
<?php
|
||||
try {
|
||||
|
||||
/* ======================================================
|
||||
1️⃣ On récupère 50 IDs récents avec photo + abonnement actif
|
||||
====================================================== */
|
||||
$ids = $pdo->query("
|
||||
SELECT id
|
||||
FROM praticien
|
||||
WHERE photo IS NOT NULL
|
||||
AND photo <> ''
|
||||
AND date_expiration > DATE_ADD(NOW(), INTERVAL 2 DAY)
|
||||
ORDER BY id DESC
|
||||
LIMIT 50
|
||||
")->fetchAll(PDO::FETCH_COLUMN);
|
||||
|
||||
if (! $ids) {
|
||||
throw new Exception('Aucun praticien');
|
||||
}
|
||||
|
||||
/* ======================================================
|
||||
2️⃣ On mélange et on prend 4
|
||||
====================================================== */
|
||||
shuffle($ids);
|
||||
$ids = array_slice($ids, 0, 4);
|
||||
|
||||
/* ======================================================
|
||||
3️⃣ On récupère les 4 profils
|
||||
====================================================== */
|
||||
$placeholders = implode(',', array_fill(0, count($ids), '?'));
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT id, nom, prenom, specialite, photo
|
||||
FROM praticien
|
||||
WHERE id IN ($placeholders)
|
||||
");
|
||||
$stmt->execute($ids);
|
||||
|
||||
$randomPros = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (! $randomPros) {
|
||||
throw new Exception('Aucun praticien');
|
||||
}
|
||||
/* ======================================================
|
||||
3️⃣ Notes
|
||||
====================================================== */
|
||||
function percent_from_5($note)
|
||||
{
|
||||
$n = max(0, min(5, (float) $note));
|
||||
return (int) round($n / 5 * 100);
|
||||
}
|
||||
|
||||
$ratingsById = [];
|
||||
|
||||
$stRatings = $pdo->prepare("
|
||||
SELECT praticien_id, ROUND(AVG(note),1) AS m, COUNT(*) AS c
|
||||
FROM avis_praticien
|
||||
WHERE statut='publie' AND praticien_id IN ($placeholders)
|
||||
GROUP BY praticien_id
|
||||
");
|
||||
$stRatings->execute($ids);
|
||||
|
||||
foreach ($stRatings->fetchAll(PDO::FETCH_ASSOC) as $r) {
|
||||
$ratingsById[(int) $r['praticien_id']] = [
|
||||
'm' => (float) $r['m'],
|
||||
'c' => (int) $r['c'],
|
||||
];
|
||||
}
|
||||
|
||||
/* ======================================================
|
||||
4️⃣ Affichage
|
||||
====================================================== */
|
||||
foreach ($randomPros as $rp):
|
||||
|
||||
$pid = (int) $rp['id'];
|
||||
|
||||
$photoName = ltrim((string) $rp['photo'], '/');
|
||||
$rpImg = "/public/uploads/" . $photoName;
|
||||
$abs = APP_HTDOCS . "/public/uploads/" . $photoName;
|
||||
|
||||
if (! $photoName || ! is_file($abs)) {
|
||||
$rpImg = "/public/uploads/img/user_placeholder.png";
|
||||
}
|
||||
|
||||
$fullName = trim(($rp['prenom'] ?? '') . ' ' . ($rp['nom'] ?? ''));
|
||||
$spec = trim((string) ($rp['specialite'] ?? ''));
|
||||
$detailUrl = "/detail_public.php?id=" . $pid;
|
||||
|
||||
$hour = random_int(9, 18);
|
||||
$availability = ($translations['home_online_at'] ?? 'Онлайн:') . ' ' . sprintf('%02d:00', $hour);
|
||||
|
||||
$stats = $ratingsById[$pid] ?? ['m' => 0, 'c' => 0];
|
||||
$note = (float) $stats['m'];
|
||||
$count = (int) $stats['c'];
|
||||
$w = percent_from_5($note);
|
||||
?>
|
||||
<article class="hp-card">
|
||||
|
||||
<div class="hp-row1">
|
||||
<a href="<?php echo h($detailUrl) ?>" class="hp-avatarLink">
|
||||
<img class="hp-avatar" src="<?php echo h($rpImg) ?>" alt="<?php echo h($fullName) ?>" width="56" height="56" loading="lazy">
|
||||
</a>
|
||||
|
||||
<div class="hp-main">
|
||||
<a href="<?php echo h($detailUrl) ?>" class="hp-avatarLink">
|
||||
<p class="hp-name"><?php echo h($fullName) ?></p>
|
||||
</a>
|
||||
<div class="hp-spec">
|
||||
<?php
|
||||
$text = $spec ?: ($translations['home_specialist_fallback'] ?? '');
|
||||
$words = preg_split('/\s+/', trim($text));
|
||||
echo h(implode(' ', array_slice($words, 0, 7)));
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hp-rating">
|
||||
<?php if ($count > 0): ?>
|
||||
<div class="hp-stars">
|
||||
<div class="stars-base">★★★★★</div>
|
||||
<div class="stars-fill" style="width:<?php echo (int) $w ?>%">★★★★★</div>
|
||||
</div>
|
||||
|
||||
<div class="hp-note">
|
||||
<?php echo number_format($note, 1, ',', ' ') ?>/5
|
||||
<span class="text-muted">(<?php echo (int) $count ?>)</span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hp-row2">
|
||||
<div class="hp-online">
|
||||
<span>📅</span>
|
||||
<span><?php echo h($availability) ?></span>
|
||||
</div>
|
||||
|
||||
<a class="hp-btn" href="<?php echo h($detailUrl) ?>">
|
||||
<?php echo h($translations['home_btn_book'] ?? '') ?>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</article>
|
||||
<?php
|
||||
endforeach;
|
||||
|
||||
} catch (Throwable $e) {
|
||||
// silencieux volontaire
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
<br><br>
|
||||
<form class="searchbar" action="/recherche" method="get" id="searchForm">
|
||||
<div class="search-icon">🔍</div>
|
||||
|
||||
<div class="input-wrap">
|
||||
<input type="text"
|
||||
id="specialite"
|
||||
name="specialite"
|
||||
autocomplete="off"
|
||||
placeholder="<?php echo h($translations['n_special'] ?? '') ?>"
|
||||
aria-label="Специальность">
|
||||
<ul class="suggest" id="sg-specialite" hidden></ul>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<div class="input-wrap">
|
||||
<input type="text"
|
||||
id="ville"
|
||||
name="ville"
|
||||
autocomplete="off"
|
||||
placeholder="<?php echo h($translations['gde'] ?? '') ?>"
|
||||
aria-label="Город">
|
||||
<ul class="suggest" id="sg-ville" hidden></ul>
|
||||
</div>
|
||||
|
||||
<button class="hp-btn btn-find--inbar" type="submit">
|
||||
<?php echo h($translations['naiti'] ?? '') ?>
|
||||
</button>
|
||||
</form><br>
|
||||
<div class="spec-cta-wrap">
|
||||
<h3><?php echo h($translations['choix_pro_titre_home'] ?? '') ?></h3>
|
||||
<p><!--<?php echo h($translations['choix_pro_texte_court'] ?? '') ?>--></p>
|
||||
<a class="spec-cta" href="/inscription_praticien.php">
|
||||
<span class="spec-cta__label"><?php echo h($translations['titre_spec'] ?? '') ?></span>
|
||||
</a>
|
||||
</div>
|
||||
<br>
|
||||
<!--<div class="hp-all">
|
||||
<a href="/result_search.php">
|
||||
<?php echo h($translations['smotret_vsex'] ?? '') ?>
|
||||
</a>
|
||||
</div>-->
|
||||
<br>
|
||||
|
||||
<!-- Articles -->
|
||||
<!--<h2 class="home-h2"><?php echo h($translations['home_useful_articles'] ?? '') ?></h2>-->
|
||||
|
||||
<?php
|
||||
function fetch_url(string $url, int $timeout = 4): array
|
||||
{
|
||||
// Retourne [ok(bool), body(string), err(string), http(int)]
|
||||
if (! function_exists('curl_init')) {
|
||||
return [false, '', 'cURL non disponible', 0];
|
||||
}
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_CONNECTTIMEOUT => $timeout,
|
||||
CURLOPT_TIMEOUT => $timeout,
|
||||
CURLOPT_USERAGENT => 'uslugi.fr (+https://staging.uslugi.fr)',
|
||||
CURLOPT_SSL_VERIFYPEER => true,
|
||||
CURLOPT_SSL_VERIFYHOST => 2,
|
||||
]);
|
||||
|
||||
$body = (string) curl_exec($ch);
|
||||
$err = curl_error($ch);
|
||||
$http = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
$ok = ($err === '' && $http >= 200 && $http < 300 && $body !== '');
|
||||
return [$ok, $body, $err, $http];
|
||||
}
|
||||
function decode_entities(string $s): string
|
||||
{
|
||||
// ENT_HTML5 gère correctement les entités modernes
|
||||
return html_entity_decode($s, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
}
|
||||
|
||||
function fetch_sovetnik_articles(int $limit = 4, bool $debug = false): array
|
||||
{
|
||||
$cacheFile = APP_HTDOCS . '/cache/sovetnik_feed.json';
|
||||
$lockFile = APP_HTDOCS . '/cache/sovetnik_feed.lock';
|
||||
|
||||
// Soft TTL: on essaye de rafraîchir après 15 min
|
||||
// Hard TTL: on accepte d'afficher un cache jusqu'à 24h (pour ne jamais bloquer la page)
|
||||
$ttlSoft = 15 * 60;
|
||||
$ttlHard = 24 * 60 * 60;
|
||||
|
||||
$now = time();
|
||||
$hasCache = is_file($cacheFile);
|
||||
$age = $hasCache ? ($now - filemtime($cacheFile)) : PHP_INT_MAX;
|
||||
|
||||
// Helper pour lire cache
|
||||
$readCache = function () use ($cacheFile, $limit): array {
|
||||
$data = json_decode((string) @file_get_contents($cacheFile), true);
|
||||
return (is_array($data) && $data) ? array_slice($data, 0, $limit) : [];
|
||||
};
|
||||
|
||||
// 1) Cache frais => retour immédiat (rapide)
|
||||
if ($hasCache && $age < $ttlSoft) {
|
||||
return $readCache();
|
||||
}
|
||||
|
||||
// 2) Cache expiré mais pas trop vieux => on le garde en "stale" (fallback instantané)
|
||||
$stale = [];
|
||||
if ($hasCache && $age < $ttlHard) {
|
||||
$stale = $readCache();
|
||||
}
|
||||
|
||||
// 3) On tente un refresh, MAIS sans bloquer longtemps
|
||||
// + lock pour éviter que plusieurs requêtes refreshent en même temps
|
||||
$canRefresh = true;
|
||||
|
||||
if (is_file($lockFile)) {
|
||||
// lock encore récent => on ne refresh pas
|
||||
if (($now - filemtime($lockFile)) < 60) {
|
||||
$canRefresh = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (! $canRefresh) {
|
||||
// On sert le stale cache (ou vide)
|
||||
return $stale;
|
||||
}
|
||||
|
||||
// On pose le lock (best-effort)
|
||||
@is_dir(dirname($lockFile)) || @mkdir(dirname($lockFile), 0755, true);
|
||||
@file_put_contents($lockFile, (string) $now);
|
||||
|
||||
// Timeout court = clé de la perf
|
||||
$timeout = 1;
|
||||
|
||||
// 1) RSS
|
||||
$feedCandidates = [
|
||||
'https://www.sovetnik.fr/feed/',
|
||||
'https://www.sovetnik.fr/?feed=rss2',
|
||||
'https://www.sovetnik.fr/feed/rss2/',
|
||||
];
|
||||
|
||||
foreach ($feedCandidates as $feedUrl) {
|
||||
[$ok, $xmlStr, $err, $http] = fetch_url($feedUrl, $timeout);
|
||||
|
||||
if ($debug) {
|
||||
echo "\n<!-- Sovetnik RSS try: $feedUrl | ok=" . ($ok ? '1' : '0') . " | http=$http | err=" . htmlspecialchars($err) . " -->\n";
|
||||
}
|
||||
|
||||
if (! $ok) {
|
||||
continue;
|
||||
}
|
||||
|
||||
libxml_use_internal_errors(true);
|
||||
$xml = simplexml_load_string($xmlStr);
|
||||
if (! $xml || empty($xml->channel->item)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$out = [];
|
||||
foreach ($xml->channel->item as $item) {
|
||||
$titleRaw = (string) $item->title;
|
||||
$descRaw = (string) $item->description;
|
||||
|
||||
$out[] = [
|
||||
'url' => (string) $item->link,
|
||||
'title' => trim(decode_entities(strip_tags($titleRaw))),
|
||||
'desc' => trim(decode_entities(strip_tags($descRaw))),
|
||||
'img' => '',
|
||||
];
|
||||
|
||||
if (count($out) >= $limit) {
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if ($out) {
|
||||
@is_dir(dirname($cacheFile)) || @mkdir(dirname($cacheFile), 0755, true);
|
||||
@file_put_contents($cacheFile, json_encode($out, JSON_UNESCAPED_UNICODE));
|
||||
@unlink($lockFile);
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Fallback API
|
||||
$apiUrl = 'https://www.sovetnik.fr/wp-json/wp/v2/posts?per_page=' . (int) $limit . '&_embed=1';
|
||||
[$ok, $jsonStr, $err, $http] = fetch_url($apiUrl, $timeout);
|
||||
|
||||
if ($debug) {
|
||||
echo "\n<!-- Sovetnik API try: $apiUrl | ok=" . ($ok ? '1' : '0') . " | http=$http | err=" . htmlspecialchars($err) . " -->\n";
|
||||
}
|
||||
|
||||
if ($ok) {
|
||||
$arr = json_decode($jsonStr, true);
|
||||
if (is_array($arr)) {
|
||||
$out = [];
|
||||
foreach ($arr as $p) {
|
||||
$title = trim(decode_entities(strip_tags($p['title']['rendered'] ?? '')));
|
||||
$desc = trim(decode_entities(strip_tags($p['excerpt']['rendered'] ?? '')));
|
||||
|
||||
$img = '';
|
||||
if (! empty($p['_embedded']['wp:featuredmedia'][0]['source_url'])) {
|
||||
$img = (string) $p['_embedded']['wp:featuredmedia'][0]['source_url'];
|
||||
}
|
||||
|
||||
$out[] = [
|
||||
'url' => (string) ($p['link'] ?? ''),
|
||||
'title' => $title,
|
||||
'desc' => $desc,
|
||||
'img' => $img,
|
||||
];
|
||||
|
||||
if (count($out) >= $limit) {
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if ($out) {
|
||||
@is_dir(dirname($cacheFile)) || @mkdir(dirname($cacheFile), 0755, true);
|
||||
@file_put_contents($cacheFile, json_encode($out, JSON_UNESCAPED_UNICODE));
|
||||
@unlink($lockFile);
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Échec refresh => on enlève le lock et on sert le stale
|
||||
@unlink($lockFile);
|
||||
return $stale;
|
||||
}
|
||||
|
||||
// Debug optionnel
|
||||
$debugArticles = isset($_GET['debug']) && $_GET['debug'] === '1';
|
||||
$articles = fetch_sovetnik_articles(4, $debugArticles);
|
||||
?>
|
||||
|
||||
|
||||
|
||||
<div class="hp-articles">
|
||||
<?php if (! empty($articles)): ?>
|
||||
<?php foreach ($articles as $a): ?>
|
||||
<a class="ha" href="<?php echo h($a['url']) ?>" target="_blank" rel="noopener">
|
||||
<div class="ha-img" style="<?php echo ! empty($a['img']) ? 'background:url(' . h($a['img']) . ') top center/cover no-repeat;' : '' ?>"></div>
|
||||
<div class="ha-body">
|
||||
<p class="ha-title"><?php echo h($a['title']) ?></p>
|
||||
<p class="ha-sub"><?php echo h(mb_strimwidth($a['desc'], 0, 120, '…', 'UTF-8')) ?></p>
|
||||
</div>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<div class="text-muted"><?php echo h($translations['home_articles_unavailable'] ?? 'Articles indisponibles pour le moment.') ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!--<div class="hp-all">
|
||||
<a href="https://www.sovetnik.fr/" target="_blank" rel="noopener">
|
||||
<?php echo h($translations['home_all_articles'] ?? '') ?>
|
||||
</a>
|
||||
</div>-->
|
||||
|
||||
<br>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
<div class="visually-hidden">
|
||||
<h2><?php echo h($translations['boostseo_home_h2_cities'] ?? '') ?></h2>
|
||||
<p><?php echo h($translations['boostseo_home_cities_text'] ?? '') ?></p>
|
||||
</div>
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p><?php echo $translations['copyr'] ?? '' ?><a href="/terms_fr.php" target="_blank"></a></p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- ✅ Scripts recherche + submit (chips = liens donc pas de JS chips) -->
|
||||
<script src="/js/home.js"></script>
|
||||
|
||||
<!-- ✅ Bootstrap -->
|
||||
<script src="/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<?php include __DIR__ . '/includes/accepter_cookies.php'; ?>
|
||||
</body>
|
||||
</html>
|
||||
+60
-60
@@ -1,34 +1,33 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/../includes/bootstrap_global.php';
|
||||
declare (strict_types = 1);
|
||||
require_once __DIR__ . '/../includes/bootstrap_global.php';
|
||||
|
||||
$message = '';
|
||||
$message = '';
|
||||
$messageType = '';
|
||||
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
|
||||
$email = trim($_POST['email'] ?? '');
|
||||
|
||||
// Email valide + pas d’injection d’en-têtes
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL) || preg_match('/[\r\n]/', $email)) {
|
||||
$message = $translations['err_email_non_valide'] ?? 'Email invalide.';
|
||||
// On garde le contrôle de format, mais message neutre côté utilisateur
|
||||
if (! filter_var($email, FILTER_VALIDATE_EMAIL) || preg_match('/[\r\n]/', $email)) {
|
||||
$message = $translations['mdp_oublie_message_neutre'] ?? 'Si un compte existe pour cette adresse, un code a été envoyé.';
|
||||
$messageType = 'success';
|
||||
} else {
|
||||
// Vérifier si le compte existe
|
||||
$stmt = $pdo->prepare("SELECT id FROM compte WHERE email = :email LIMIT 1");
|
||||
$stmt->execute(['email' => $email]);
|
||||
$compte = $stmt->fetch();
|
||||
|
||||
if ($compte) {
|
||||
// Génère un code à 6 chiffres
|
||||
$code = random_int(100000, 999999);
|
||||
|
||||
// Enregistre en session (TTL 10 minutes)
|
||||
$_SESSION['reset_email'] = $email;
|
||||
$_SESSION['reset_code'] = $code;
|
||||
$_SESSION['reset_time'] = time();
|
||||
$_SESSION['reset_ttl'] = 600; // 10 min
|
||||
$_SESSION['reset_email'] = $email;
|
||||
$_SESSION['reset_code'] = (string) $code;
|
||||
$_SESSION['reset_time'] = time();
|
||||
$_SESSION['reset_ttl'] = 600;
|
||||
$_SESSION['reset_attempts'] = 0;
|
||||
|
||||
// Prépare l'e-mail
|
||||
$sujet = $translations['mail_sujet_code'];
|
||||
$codeHtml = htmlspecialchars((string)$code, ENT_QUOTES, 'UTF-8');
|
||||
$sujet = $translations['mail_sujet_code'] ?? 'Code de réinitialisation';
|
||||
$codeHtml = htmlspecialchars((string) $code, ENT_QUOTES, 'UTF-8');
|
||||
|
||||
$html = "<!DOCTYPE html>
|
||||
<html lang='fr'>
|
||||
@@ -44,66 +43,62 @@ if (($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
|
||||
</head>
|
||||
<body>
|
||||
<div class='mail-container'>
|
||||
<div class='title'>".$translations['mail_bonjour']."</div>
|
||||
<p>".$translations['mail_phrase_code']."</p>
|
||||
<div class='code'>".$codeHtml."</div>
|
||||
<p>".$translations['mail_phrase_validite']."</p>
|
||||
<div class='footer'>".$translations['mail_signature']."</div>
|
||||
<div class='title'>" . htmlspecialchars($translations['mail_bonjour'] ?? 'Bonjour', ENT_QUOTES, 'UTF-8') . "</div>
|
||||
<p>" . htmlspecialchars($translations['mail_phrase_code'] ?? 'Voici votre code de réinitialisation :', ENT_QUOTES, 'UTF-8') . "</p>
|
||||
<div class='code'>" . $codeHtml . "</div>
|
||||
<p>" . htmlspecialchars($translations['mail_phrase_validite'] ?? 'Ce code est valable pendant un temps limité.', ENT_QUOTES, 'UTF-8') . "</p>
|
||||
<div class='footer'>" . htmlspecialchars($translations['mail_signature'] ?? 'USLUGI.FR', ENT_QUOTES, 'UTF-8') . "</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>";
|
||||
|
||||
$text = strip_tags(
|
||||
$translations['mail_bonjour']."\n\n".
|
||||
$translations['mail_phrase_code']."\n".
|
||||
$code."\n".
|
||||
$translations['mail_phrase_validite']."\n\n".
|
||||
$translations['mail_signature']
|
||||
($translations['mail_bonjour'] ?? 'Bonjour') . "\n\n" .
|
||||
($translations['mail_phrase_code'] ?? 'Voici votre code de réinitialisation :') . "\n" .
|
||||
$code . "\n" .
|
||||
($translations['mail_phrase_validite'] ?? 'Ce code est valable pendant un temps limité.') . "\n\n" .
|
||||
($translations['mail_signature'] ?? 'USLUGI.FR')
|
||||
);
|
||||
|
||||
// Envoi via PHPMailer helper
|
||||
list($ok, $err) = uslugi_mail($email, $sujet, $html, $text);
|
||||
if (!$ok) {
|
||||
|
||||
if (! $ok) {
|
||||
error_log("Reset code mail NOT sent to $email : $err");
|
||||
// On reste silencieux pour l’utilisateur
|
||||
// on reste neutre côté utilisateur
|
||||
}
|
||||
|
||||
// On peut rediriger seulement si le compte existe
|
||||
header("Location: verifier_code.php");
|
||||
exit;
|
||||
} else {
|
||||
// Ton message existant si l'email est inconnu
|
||||
$message = $translations['erreur_email_inconnu'];
|
||||
}
|
||||
|
||||
// Email inconnu → message neutre, pas de détail
|
||||
$message = $translations['mdp_oublie_message_neutre'] ?? 'Si un compte existe pour cette adresse, un code a été envoyé.';
|
||||
$messageType = 'success';
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title><?= $translations['mdp_oublie_title'] ?></title>
|
||||
<title><?php echo htmlspecialchars($translations['mdp_oublie_title'] ?? 'Mot de passe oublié', ENT_QUOTES, 'UTF-8') ?></title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="/css/bootstrap.min.css" rel="stylesheet">
|
||||
<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') ?>">
|
||||
<link rel="stylesheet" href="<?php echo asset('/includes/site_style.css') ?>">
|
||||
<link rel="stylesheet" href="<?php echo asset('/includes/custom_style.css') ?>">
|
||||
<link rel="stylesheet" href="<?php echo asset('/includes/mobil_style.css') ?>">
|
||||
<?php include_once __DIR__ . '/includes/tracking.php'; ?>
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<div class="container">
|
||||
<div class="header-left">
|
||||
<div class="brand">
|
||||
<a href="/index.php"><img src="/public/uploads/img/logo_u.png"
|
||||
alt="uslugi.fr"
|
||||
class="logo-mobile"></a>
|
||||
|
||||
<a href="/index.php"><img src="/public/uploads/img/logo.png"
|
||||
alt="uslugi.fr"
|
||||
class="logo-desktop"></a>
|
||||
</div>
|
||||
<div class="lang-switch">
|
||||
<div class="brand">
|
||||
<a href="/index.php"><img src="/public/uploads/img/logo_u.png" alt="uslugi.fr" class="logo-mobile"></a>
|
||||
<a href="/index.php"><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>
|
||||
@@ -113,37 +108,42 @@ if (($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
|
||||
type="button" data-bs-toggle="dropdown" data-bs-auto-close="true"
|
||||
aria-expanded="false" aria-label="Помощь">?</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end shadow help-menu menu-panel">
|
||||
<li><a href="/how-it-work.php"><?= $translations['kak_rabotaet'] ?></a></a></li>
|
||||
<li><a href="/about.php"><?= $translations['pro_nas'] ?></a></li>
|
||||
<li><a href="/faq.php"><?= $translations['faq'] ?></a></li>
|
||||
<li><a href="/contact.php"><?= $translations['nam_pisat'] ?></a></li>
|
||||
<li><a href="/how-it-work.php"><?php echo $translations['kak_rabotaet'] ?></a></li>
|
||||
<li><a href="/about.php"><?php echo $translations['pro_nas'] ?></a></li>
|
||||
<li><a href="/faq.php"><?php echo $translations['faq'] ?></a></li>
|
||||
<li><a href="/contact.php"><?php echo $translations['nam_pisat'] ?></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="main">
|
||||
<div class="card shadow-sm p-4" style="min-width:380px; max-width:520px; margin:40px auto;">
|
||||
<h3 class="mb-4 text-center"><?= $translations['mdp_oublie_h3'] ?></h3>
|
||||
<h3 class="mb-4 text-center"><?php echo htmlspecialchars($translations['mdp_oublie_h3'] ?? 'Mot de passe oublié', ENT_QUOTES, 'UTF-8') ?></h3>
|
||||
|
||||
<?php if (!empty($message)): ?>
|
||||
<div class="alert alert-danger py-2"><?= htmlspecialchars($message, ENT_QUOTES, 'UTF-8') ?></div>
|
||||
<?php if (! empty($message)): ?>
|
||||
<div class="alert alert-<?php echo $messageType === 'success' ? 'success' : 'danger' ?> py-2">
|
||||
<?php echo htmlspecialchars($message, ENT_QUOTES, 'UTF-8') ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="POST" novalidate>
|
||||
<div class="mb-3">
|
||||
<input type="email" name="email" class="form-control" placeholder="<?= $translations['placeholder_email'] ?>" required>
|
||||
<input type="email" name="email" class="form-control"
|
||||
placeholder="<?php echo htmlspecialchars($translations['placeholder_email'] ?? 'Email', ENT_QUOTES, 'UTF-8') ?>"
|
||||
required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100"><?= $translations['btn_envoyer_code'] ?></button>
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
<?php echo htmlspecialchars($translations['btn_envoyer_code'] ?? 'Envoyer le code', ENT_QUOTES, 'UTF-8') ?>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p><?= $translations['copyr'] ?></p>
|
||||
<p><?php echo $translations['copyr'] ?></p>
|
||||
</div>
|
||||
</footer>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
+307
-190
@@ -1,248 +1,358 @@
|
||||
<?php
|
||||
// /public/cabinet/disponibilites.php (VERSION SECRÉTAIRE)
|
||||
require_once __DIR__ . '/../includes/init_secretaire.php';
|
||||
declare (strict_types = 1);
|
||||
|
||||
// /public/cabinet/disponibilites.php
|
||||
require_once __DIR__ . '/../includes/init_secretaire.php';
|
||||
|
||||
// --- Helpers
|
||||
if (!function_exists('h')) { function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); } }
|
||||
if (! function_exists('h')) {
|
||||
function h($s): string
|
||||
{
|
||||
return htmlspecialchars((string) $s, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
}
|
||||
|
||||
// --- Contexte praticien obligatoire (depuis le lien du dashboard)
|
||||
$compteId = (int)($_SESSION['compte_id'] ?? 0);
|
||||
$userRole = strtolower((string)($_SESSION['role'] ?? ''));
|
||||
$praticien_id = isset($_GET['praticien_id']) ? (int)$_GET['praticien_id'] : 0;
|
||||
if ($praticien_id <= 0 || $userRole !== 'secretaire') { http_response_code(403); exit('Forbidden'); }
|
||||
$compteId = (int) ($_SESSION['compte_id'] ?? 0);
|
||||
$userRole = strtolower((string) ($_SESSION['role'] ?? ''));
|
||||
$praticien_id = (int) ($_GET['praticien_id'] ?? 0);
|
||||
|
||||
// --- Vérifier pivot + droit can_edit_slots
|
||||
$st = $pdo->prepare("
|
||||
SELECT can_edit_slots
|
||||
if ($praticien_id <= 0 || $userRole !== 'secretaire') {
|
||||
http_response_code(403);
|
||||
exit('Forbidden');
|
||||
}
|
||||
|
||||
// Vérifier pivot + droit
|
||||
$st = $pdo->prepare("
|
||||
SELECT can_edit_slots
|
||||
FROM secretaire_praticien
|
||||
WHERE secretaire_user_id = :u
|
||||
AND praticien_id = :p
|
||||
AND is_active = 1
|
||||
LIMIT 1
|
||||
WHERE secretaire_user_id = :u
|
||||
AND praticien_id = :p
|
||||
AND is_active = 1
|
||||
LIMIT 1
|
||||
");
|
||||
$st->execute(['u'=>$compteId,'p'=>$praticien_id]);
|
||||
$pivot = $st->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$pivot || (int)$pivot['can_edit_slots'] !== 1) { http_response_code(403); exit('Forbidden'); }
|
||||
$st->execute([
|
||||
'u' => $compteId,
|
||||
'p' => $praticien_id,
|
||||
]);
|
||||
$pivot = $st->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
// ---- Ajouter un créneau (POST)
|
||||
$erreur = null;
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['jour'], $_POST['heure_debut'], $_POST['heure_fin'])) {
|
||||
$jour = trim((string)$_POST['jour']);
|
||||
$debut = trim((string)$_POST['heure_debut']);
|
||||
$fin = trim((string)$_POST['heure_fin']);
|
||||
if (! $pivot || (int) $pivot['can_edit_slots'] !== 1) {
|
||||
http_response_code(403);
|
||||
exit('Forbidden');
|
||||
}
|
||||
|
||||
// mini validation
|
||||
$joursValides = ['lundi','mardi','mercredi','jeudi','vendredi','samedi','dimanche'];
|
||||
if (!in_array($jour, $joursValides, true)) {
|
||||
$erreur = $translations['erreur_jour_invalide'] ?? "Jour invalide.";
|
||||
} elseif (!preg_match('#^\d{2}:\d{2}$#', $debut) || !preg_match('#^\d{2}:\d{2}$#', $fin)) {
|
||||
$erreur = $translations['erreur_format_heure'] ?? "Format d’heure invalide.";
|
||||
} elseif ($debut >= $fin) {
|
||||
// Récup nom praticien pour le titre
|
||||
$st = $pdo->prepare("SELECT prenom, nom, duree_rdv FROM praticien WHERE id = ? LIMIT 1");
|
||||
$st->execute([$praticien_id]);
|
||||
$praticien = $st->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (! $praticien) {
|
||||
http_response_code(404);
|
||||
exit('Praticien introuvable');
|
||||
}
|
||||
|
||||
$currentPraticienName = trim(((string) ($praticien['prenom'] ?? '')) . ' ' . ((string) ($praticien['nom'] ?? '')));
|
||||
$duree_minutes = (int) ($praticien['duree_rdv'] ?? 0);
|
||||
if ($duree_minutes <= 0) {
|
||||
$duree_minutes = 30;
|
||||
}
|
||||
|
||||
$erreur = null;
|
||||
|
||||
$toMin = function (string $hhmm): ?int {
|
||||
if (! preg_match('/^\d{2}:\d{2}$/', $hhmm)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
[$h, $m] = array_map('intval', explode(':', $hhmm));
|
||||
if ($h < 0 || $h > 23 || $m < 0 || $m > 59) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $h * 60 + $m;
|
||||
};
|
||||
|
||||
// ---- Ajouter un créneau (POST + CSRF)
|
||||
if (
|
||||
$_SERVER['REQUEST_METHOD'] === 'POST'
|
||||
&& ($_POST['action'] ?? '') === 'add'
|
||||
&& isset($_POST['jour'], $_POST['heure_debut'], $_POST['heure_fin'])
|
||||
) {
|
||||
require_post_csrf();
|
||||
|
||||
$jour = trim((string) $_POST['jour']);
|
||||
$debut = trim((string) $_POST['heure_debut']);
|
||||
$fin = trim((string) $_POST['heure_fin']);
|
||||
|
||||
$joursValides = ['lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi', 'dimanche'];
|
||||
$md = $toMin($debut);
|
||||
$mf = $toMin($fin);
|
||||
|
||||
if (! in_array($jour, $joursValides, true)) {
|
||||
$erreur = $translations['erreur_jour_invalide'] ?? 'Jour invalide.';
|
||||
} elseif ($md === null || $mf === null) {
|
||||
$erreur = $translations['erreur_format_heure'] ?? "Format d'heure invalide.";
|
||||
} elseif ($md >= $mf) {
|
||||
$erreur = $translations['erreur_debut_avant_fin'] ?? "Heure de début après l’heure de fin.";
|
||||
}
|
||||
|
||||
if (!$erreur) {
|
||||
$stmt = $pdo->prepare(
|
||||
"INSERT INTO disponibilite (praticien_id, jour_semaine, heure_debut, heure_fin)
|
||||
VALUES (:p, :j, :hd, :hf)"
|
||||
);
|
||||
$stmt->execute([
|
||||
'p'=>$praticien_id,
|
||||
'j'=>$jour,
|
||||
'hd'=>$debut,
|
||||
'hf'=>$fin
|
||||
]);
|
||||
header('Location: /public/cabinet/disponibilites.php?praticien_id='.$praticien_id.'&ok=added', true, 303);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Supprimer un créneau (GET)
|
||||
// (idéalement: POST + CSRF, on garde GET pour coller à l’original, avec une vérif d’appartenance)
|
||||
if (isset($_GET['supprimer'])) {
|
||||
$id = (int)$_GET['supprimer'];
|
||||
|
||||
// Vérifier que ce slot appartient bien à ce praticien
|
||||
$own = $pdo->prepare("SELECT 1 FROM disponibilite WHERE id = :id AND praticien_id = :p LIMIT 1");
|
||||
$own->execute(['id'=>$id,'p'=>$praticien_id]);
|
||||
if ($own->fetchColumn()) {
|
||||
$stmt = $pdo->prepare("DELETE FROM disponibilite WHERE id = :id AND praticien_id = :p");
|
||||
$stmt->execute(['id'=>$id,'p'=>$praticien_id]);
|
||||
header('Location: /public/cabinet/disponibilites.php?praticien_id='.$praticien_id.'&ok=deleted', true, 303);
|
||||
exit;
|
||||
} else {
|
||||
http_response_code(403); exit('Forbidden');
|
||||
$window = $mf - $md;
|
||||
|
||||
if ($window < $duree_minutes) {
|
||||
$erreur = sprintf(
|
||||
$translations['erreur_plage_trop_courte'] ?? "Créneau trop court (%d min). Minimum : %d min.",
|
||||
$window,
|
||||
$duree_minutes
|
||||
);
|
||||
} elseif (($window % $duree_minutes) !== 0) {
|
||||
$erreur = sprintf(
|
||||
$translations['erreur_plage_incompatible_duree'] ?? "Créneau incompatible. La durée doit être un multiple de %d min.",
|
||||
$duree_minutes
|
||||
);
|
||||
} else {
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO disponibilite (praticien_id, jour_semaine, heure_debut, heure_fin)
|
||||
VALUES (:p, :j, :hd, :hf)
|
||||
");
|
||||
$stmt->execute([
|
||||
'p' => $praticien_id,
|
||||
'j' => $jour,
|
||||
'hd' => $debut,
|
||||
'hf' => $fin,
|
||||
]);
|
||||
|
||||
header('Location: /public/cabinet/disponibilites.php?praticien_id=' . $praticien_id . '&ok=added', true, 303);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Récupérer les créneaux
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT * FROM disponibilite
|
||||
WHERE praticien_id = ?
|
||||
ORDER BY FIELD(jour_semaine,'lundi','mardi','mercredi','jeudi','vendredi','samedi','dimanche'), heure_debut"
|
||||
);
|
||||
$stmt->execute([$praticien_id]);
|
||||
$dispos = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
// ---- Récupérer les créneaux
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT *
|
||||
FROM disponibilite
|
||||
WHERE praticien_id = ?
|
||||
ORDER BY FIELD(jour_semaine,'lundi','mardi','mercredi','jeudi','vendredi','samedi','dimanche'), heure_debut
|
||||
");
|
||||
$stmt->execute([$praticien_id]);
|
||||
$dispos = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$jourKeyMap = [
|
||||
'lundi' => 'jour_lundi',
|
||||
'mardi' => 'jour_mardi',
|
||||
'mercredi' => 'jour_mercredi',
|
||||
'jeudi' => 'jour_jeudi',
|
||||
'vendredi' => 'jour_vendredi',
|
||||
'samedi' => 'jour_samedi',
|
||||
'dimanche' => 'jour_dimanche',
|
||||
];
|
||||
|
||||
// Prépare les traductions de jours
|
||||
$jourKeyMap = [
|
||||
'lundi'=>'jour_lundi','mardi'=>'jour_mardi','mercredi'=>'jour_mercredi',
|
||||
'jeudi'=>'jour_jeudi','vendredi'=>'jour_vendredi','samedi'=>'jour_samedi','dimanche'=>'jour_dimanche',
|
||||
];
|
||||
$PAGE_TITRE = $translations['titre_disponibilites_date'] ?? 'Disponibilités (dates)';
|
||||
$HEAD_EXTRA = <<<HTML
|
||||
<script src="/js/sweetalert2@11.js"></script>
|
||||
$PAGE_TITLE = $translations['titre_disponibilites'] ?? 'Disponibilités';
|
||||
$HEAD_EXTRA = <<<HTML
|
||||
<script src="/js/sweetalert2@11.js"></script>
|
||||
HTML;
|
||||
require_once __DIR__ . '/../includes/header_cabinet.php';
|
||||
|
||||
require_once __DIR__ . '/../includes/header_cabinet.php';
|
||||
?>
|
||||
|
||||
<div class="main1">
|
||||
<div class="container py-5">
|
||||
<div class="shadow p-4 rounded bg-light">
|
||||
<h2 class="mb-4 text-center"><?= htmlspecialchars($currentPraticienName, ENT_QUOTES, 'UTF-8') ?> - <?= h($translations['sloti'] ?? 'Mes disponibilités') ?></h2>
|
||||
<div class="text-end">
|
||||
<a href="/public/cabinet/disponibilites_date.php?praticien_id=<?= (int)$praticien_id ?>" class="btn btn-outline-secondary mb-4">
|
||||
<?= htmlspecialchars($translations['btn_ajouter_par_date'] ?? 'Ajouter par date') ?>
|
||||
</a>
|
||||
</div>
|
||||
<div class="table-responsive mb-4">
|
||||
<table class="table table-bordered table-striped align-middle text-center mobile-cards" id="table_rdv">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th><?= h($translations['table_jour'] ?? 'Jour') ?></th>
|
||||
<th><?= h($translations['table_debut'] ?? 'Début') ?></th>
|
||||
<th><?= h($translations['table_fin'] ?? 'Fin') ?></th>
|
||||
<th><?= h($translations['table_action'] ?? 'Action') ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if ($dispos): foreach ($dispos as $d): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<?php
|
||||
$key = $jourKeyMap[$d['jour_semaine']] ?? null;
|
||||
echo h($key ? ($translations[$key] ?? ucfirst($d['jour_semaine'])) : ucfirst($d['jour_semaine']));
|
||||
?>
|
||||
</td>
|
||||
<td><?= h(substr((string)$d['heure_debut'], 0, 5)) ?></td>
|
||||
<td><?= h(substr((string)$d['heure_fin'], 0, 5)) ?></td>
|
||||
<td>
|
||||
<button type="button"
|
||||
<div class="container py-5">
|
||||
<div class="shadow p-4 rounded bg-light">
|
||||
<h2 class="mb-4 text-center">
|
||||
<?php echo h($currentPraticienName) ?> - <?php echo h($translations['sloti'] ?? 'Mes disponibilités') ?>
|
||||
</h2>
|
||||
|
||||
<div class="text-end">
|
||||
<a href="/public/cabinet/disponibilites_date.php?praticien_id=<?php echo (int) $praticien_id ?>" class="btn btn-outline-secondary mb-4">
|
||||
<?php echo h($translations['btn_ajouter_par_date'] ?? 'Ajouter par date') ?>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive mb-4">
|
||||
<table class="table table-bordered table-striped align-middle text-center mobile-cards" id="table_rdv">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th><?php echo h($translations['table_jour'] ?? 'Jour') ?></th>
|
||||
<th><?php echo h($translations['table_debut'] ?? 'Début') ?></th>
|
||||
<th><?php echo h($translations['table_fin'] ?? 'Fin') ?></th>
|
||||
<th><?php echo h($translations['table_action'] ?? 'Action') ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if ($dispos): ?>
|
||||
<?php foreach ($dispos as $d): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<?php
|
||||
$key = $jourKeyMap[$d['jour_semaine']] ?? null;
|
||||
echo h($key ? ($translations[$key] ?? ucfirst($d['jour_semaine'])) : ucfirst($d['jour_semaine']));
|
||||
?>
|
||||
</td>
|
||||
<td><?php echo h(substr((string) $d['heure_debut'], 0, 5)) ?></td>
|
||||
<td><?php echo h(substr((string) $d['heure_fin'], 0, 5)) ?></td>
|
||||
<td>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-danger js-del"
|
||||
data-id="<?= (int)$d['id'] ?>">
|
||||
<?= h($translations['btn_supprimer'] ?? 'Supprimer') ?>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; else: ?>
|
||||
<tr><td colspan="4"><?= h($translations['aucun_creneau'] ?? 'Aucun créneau.') ?></td></tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
data-id="<?php echo (int) $d['id'] ?>">
|
||||
<?php echo h($translations['btn_supprimer'] ?? 'Supprimer') ?>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr>
|
||||
<td colspan="4"><?php echo h($translations['aucun_creneau'] ?? 'Aucun créneau.') ?></td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm p-4">
|
||||
<h4 class="mb-3 text-center"><?= h($translations['card_ajouter_creneau'] ?? 'Ajouter un créneau') ?></h4>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="praticien_id" value="<?= (int)$praticien_id ?>">
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="jour"><?= h($translations['label_jour'] ?? 'Jour') ?></label>
|
||||
<select name="jour" id="jour" class="form-select" required>
|
||||
<option value=""><?= h($translations['option_choisir'] ?? '— Choisir —') ?></option>
|
||||
<?php
|
||||
$jours = [
|
||||
'lundi' => $translations['jour_lundi'] ?? 'Lundi',
|
||||
'mardi' => $translations['jour_mardi'] ?? 'Mardi',
|
||||
'mercredi' => $translations['jour_mercredi'] ?? 'Mercredi',
|
||||
'jeudi' => $translations['jour_jeudi'] ?? 'Jeudi',
|
||||
'vendredi' => $translations['jour_vendredi'] ?? 'Vendredi',
|
||||
'samedi' => $translations['jour_samedi'] ?? 'Samedi',
|
||||
'dimanche' => $translations['jour_dimanche'] ?? 'Dimanche',
|
||||
];
|
||||
foreach ($jours as $val => $label):
|
||||
?>
|
||||
<option value="<?= h($val) ?>"><?= h($label) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="card shadow-sm p-4">
|
||||
<h4 class="mb-3 text-center"><?php echo h($translations['card_ajouter_creneau'] ?? 'Ajouter un créneau') ?></h4>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="heure_debut"><?= h($translations['label_heure_debut'] ?? 'Heure de début') ?></label>
|
||||
<input type="time" name="heure_debut" id="heure_debut" class="form-control" required>
|
||||
</div>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="action" value="add">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo h(ensure_csrf_token()) ?>">
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="heure_fin"><?= h($translations['label_heure_fin'] ?? 'Heure de fin') ?></label>
|
||||
<input type="time" name="heure_fin" id="heure_fin" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="jour"><?php echo h($translations['label_jour'] ?? 'Jour') ?></label>
|
||||
<select name="jour" id="jour" class="form-select" required>
|
||||
<option value=""><?php echo h($translations['option_choisir'] ?? '— Choisir —') ?></option>
|
||||
<?php
|
||||
$jours = [
|
||||
'lundi' => $translations['jour_lundi'] ?? 'Lundi',
|
||||
'mardi' => $translations['jour_mardi'] ?? 'Mardi',
|
||||
'mercredi' => $translations['jour_mercredi'] ?? 'Mercredi',
|
||||
'jeudi' => $translations['jour_jeudi'] ?? 'Jeudi',
|
||||
'vendredi' => $translations['jour_vendredi'] ?? 'Vendredi',
|
||||
'samedi' => $translations['jour_samedi'] ?? 'Samedi',
|
||||
'dimanche' => $translations['jour_dimanche'] ?? 'Dimanche',
|
||||
];
|
||||
foreach ($jours as $val => $label):
|
||||
?>
|
||||
<option value="<?php echo h($val) ?>"><?php echo h($label) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
<?= h($translations['btn_ajouter'] ?? 'Ajouter') ?>
|
||||
</button>
|
||||
</form>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="heure_debut"><?php echo h($translations['label_heure_debut'] ?? 'Heure de début') ?></label>
|
||||
<input type="time" name="heure_debut" id="heure_debut" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="heure_fin"><?php echo h($translations['label_heure_fin'] ?? 'Heure de fin') ?></label>
|
||||
<input type="time" name="heure_fin" id="heure_fin" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
<?php echo h($translations['btn_ajouter'] ?? 'Ajouter') ?>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// --- SweetAlert2: messages succès / erreur depuis PHP ---
|
||||
document.addEventListener('DOMContentLoaded', function(){
|
||||
const okParam = '<?= isset($_GET['ok']) ? addslashes((string)$_GET['ok']) : '' ?>';
|
||||
const erreurMsg = '<?= $erreur ? addslashes((string)$erreur) : '' ?>';
|
||||
const csrfToken = <?php echo json_encode(ensure_csrf_token(), JSON_UNESCAPED_UNICODE) ?>;
|
||||
const praticienId = <?php echo (int) $praticien_id ?>;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const okParam = <?php echo json_encode(isset($_GET['ok']) ? (string) $_GET['ok'] : '', JSON_UNESCAPED_UNICODE) ?>;
|
||||
const erreurMsg = <?php echo json_encode($erreur ?: '', JSON_UNESCAPED_UNICODE) ?>;
|
||||
|
||||
if (okParam === 'added') {
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: '<?= addslashes($translations["msg_creneau_ajoute"] ?? "Créneau ajouté.") ?>',
|
||||
timer: 1800,
|
||||
showConfirmButton: false
|
||||
});
|
||||
} else if (okParam === 'deleted') {
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: '<?= addslashes($translations["msg_creneau_supprime"] ?? "Créneau supprimé.") ?>',
|
||||
title: <?php echo json_encode($translations['msg_creneau_ajoute'] ?? 'Créneau ajouté.', JSON_UNESCAPED_UNICODE) ?>,
|
||||
timer: 1800,
|
||||
showConfirmButton: false
|
||||
});
|
||||
|
||||
if (window.history.replaceState) {
|
||||
window.history.replaceState({}, document.title, window.location.pathname + '?praticien_id=' + encodeURIComponent(praticienId));
|
||||
}
|
||||
}
|
||||
|
||||
if (erreurMsg) {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: '<?= addslashes($translations["erreur_titre"] ?? "Erreur") ?>',
|
||||
title: <?php echo json_encode($translations['erreur_titre'] ?? 'Erreur', JSON_UNESCAPED_UNICODE) ?>,
|
||||
text: erreurMsg,
|
||||
confirmButtonText: 'OK'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// --- Confirmation de suppression avec SweetAlert2 ---
|
||||
document.addEventListener('click', async function(e){
|
||||
document.addEventListener('click', async function (e) {
|
||||
const btn = e.target.closest('.js-del');
|
||||
if (!btn) return;
|
||||
|
||||
const id = btn.dataset.id;
|
||||
if (!id) return;
|
||||
|
||||
const result = await Swal.fire({
|
||||
icon: 'warning',
|
||||
title: '<?= addslashes($translations["confirm_title"] ?? "Confirmation") ?>',
|
||||
text: '<?= addslashes($translations["confirm_supprimer_creneau"] ?? "Supprimer ce créneau ?") ?>',
|
||||
title: <?php echo json_encode($translations['confirm_title'] ?? 'Confirmation', JSON_UNESCAPED_UNICODE) ?>,
|
||||
text: <?php echo json_encode($translations['confirm_supprimer_creneau'] ?? 'Supprimer ce créneau ?', JSON_UNESCAPED_UNICODE) ?>,
|
||||
showCancelButton: true,
|
||||
confirmButtonText: '<?= addslashes($translations["btn_da_udalit"] ?? "Oui") ?>',
|
||||
cancelButtonText: '<?= addslashes($translations["btn_annuler"] ?? "Annuler") ?>',
|
||||
confirmButtonText: <?php echo json_encode($translations['btn_da_udalit'] ?? 'Oui', JSON_UNESCAPED_UNICODE) ?>,
|
||||
cancelButtonText: <?php echo json_encode($translations['btn_annuler'] ?? 'Annuler', JSON_UNESCAPED_UNICODE) ?>,
|
||||
reverseButtons: true
|
||||
});
|
||||
|
||||
if (result.isConfirmed) {
|
||||
// GET comme dans la page d’origine, on garde le contexte praticien_id
|
||||
const pid = '<?= (int)$praticien_id ?>';
|
||||
window.location.href = '?praticien_id=' + encodeURIComponent(pid) + '&supprimer=' + encodeURIComponent(id);
|
||||
}
|
||||
if (!result.isConfirmed) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('id', String(id));
|
||||
formData.append('praticien_id', String(praticienId));
|
||||
formData.append('csrf_token', csrfToken);
|
||||
|
||||
fetch('/public/cabinet/supprimer_disponibilite.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: formData
|
||||
})
|
||||
.then(async r => {
|
||||
const text = await r.text();
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch (e) {
|
||||
return { ok: false, message: text };
|
||||
}
|
||||
})
|
||||
.then(data => {
|
||||
if (data.ok) {
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: <?php echo json_encode($translations['msg_creneau_supprime'] ?? 'Créneau supprimé.', JSON_UNESCAPED_UNICODE) ?>,
|
||||
timer: 1400,
|
||||
showConfirmButton: false
|
||||
}).then(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
} else {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: <?php echo json_encode($translations['erreur_titre'] ?? 'Erreur', JSON_UNESCAPED_UNICODE) ?>,
|
||||
text: data.message || 'Suppression impossible.'
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'Erreur réseau',
|
||||
text: 'Veuillez réessayer.'
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<!--*********Adaptation des tables pour un affichage mobile***************-->
|
||||
<script defer>
|
||||
(function(){
|
||||
function initMobileCards(tbl){
|
||||
@@ -255,15 +365,22 @@ document.addEventListener('click', async function(e){
|
||||
});
|
||||
document.documentElement.classList.add('mc-ready');
|
||||
}
|
||||
function ready(fn){ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fn); else fn(); }
|
||||
ready(function(){ initMobileCards(document.getElementById('table_rdv')); });
|
||||
|
||||
function ready(fn){
|
||||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fn);
|
||||
else fn();
|
||||
}
|
||||
|
||||
ready(function(){
|
||||
initMobileCards(document.getElementById('table_rdv'));
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p><?= $translations['copyr'] ?? '© '.date('Y') ?></p>
|
||||
<p><?php echo $translations['copyr'] ?? '© ' . date('Y') ?></p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
@@ -1,52 +1,78 @@
|
||||
<?php
|
||||
// /public/cabinet/disponibilites_date.php (VERSION SECRÉTAIRE)
|
||||
require_once __DIR__ . '/../includes/init_secretaire.php';
|
||||
declare (strict_types = 1);
|
||||
|
||||
// /public/cabinet/disponibilites_date.php
|
||||
require_once __DIR__ . '/../includes/init_secretaire.php';
|
||||
|
||||
if (!function_exists('h')) {
|
||||
function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
|
||||
}
|
||||
if (! function_exists('h')) {
|
||||
function h($s): string
|
||||
{
|
||||
return htmlspecialchars((string) $s, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
}
|
||||
|
||||
// --- Contexte praticien obligatoire
|
||||
$compteId = (int)($_SESSION['compte_id'] ?? 0);
|
||||
$userRole = strtolower((string)($_SESSION['role'] ?? ''));
|
||||
$praticien_id = isset($_GET['praticien_id']) ? (int)$_GET['praticien_id'] : 0;
|
||||
// --- Contexte praticien obligatoire
|
||||
$compteId = (int) ($_SESSION['compte_id'] ?? 0);
|
||||
$userRole = strtolower((string) ($_SESSION['role'] ?? ''));
|
||||
$praticien_id = (int) ($_GET['praticien_id'] ?? 0);
|
||||
|
||||
if ($praticien_id <= 0 || $userRole !== 'secretaire') { http_response_code(403); exit('Forbidden'); }
|
||||
if ($praticien_id <= 0 || $userRole !== 'secretaire') {
|
||||
http_response_code(403);
|
||||
exit('Forbidden');
|
||||
}
|
||||
|
||||
// --- Vérifier pivot + droit can_edit_slots
|
||||
$st = $pdo->prepare("
|
||||
SELECT can_edit_slots
|
||||
// --- Vérifier pivot + droit can_edit_slots
|
||||
$st = $pdo->prepare("
|
||||
SELECT can_edit_slots
|
||||
FROM secretaire_praticien
|
||||
WHERE secretaire_user_id = :u
|
||||
AND praticien_id = :p
|
||||
AND is_active = 1
|
||||
LIMIT 1
|
||||
WHERE secretaire_user_id = :u
|
||||
AND praticien_id = :p
|
||||
AND is_active = 1
|
||||
LIMIT 1
|
||||
");
|
||||
$st->execute(['u'=>$compteId,'p'=>$praticien_id]);
|
||||
$pivot = $st->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$pivot || (int)$pivot['can_edit_slots'] !== 1) { http_response_code(403); exit('Forbidden'); }
|
||||
$st->execute([
|
||||
'u' => $compteId,
|
||||
'p' => $praticien_id,
|
||||
]);
|
||||
$pivot = $st->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
// --- Optionnel: récupérer nom praticien pour titre
|
||||
$st = $pdo->prepare("SELECT nom, prenom FROM praticien WHERE id = ? LIMIT 1");
|
||||
$st->execute([$praticien_id]);
|
||||
$prat = $st->fetch(PDO::FETCH_ASSOC);
|
||||
$currentPraticienName = trim(($prat['prenom'] ?? '').' '.($prat['nom'] ?? ''));
|
||||
if (! $pivot || (int) $pivot['can_edit_slots'] !== 1) {
|
||||
http_response_code(403);
|
||||
exit('Forbidden');
|
||||
}
|
||||
|
||||
$erreur = null;
|
||||
// --- Nom praticien
|
||||
$st = $pdo->prepare("SELECT nom, prenom FROM praticien WHERE id = ? LIMIT 1");
|
||||
$st->execute([$praticien_id]);
|
||||
$prat = $st->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
// ---- Ajouter une plage datée (POST)
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['date_dispo'], $_POST['heure_debut'], $_POST['heure_fin'])) {
|
||||
$date = trim((string)$_POST['date_dispo']); // YYYY-MM-DD
|
||||
$debut = trim((string)$_POST['heure_debut']); // HH:MM
|
||||
$fin = trim((string)$_POST['heure_fin']); // HH:MM
|
||||
if (! $prat) {
|
||||
http_response_code(404);
|
||||
exit('Praticien introuvable');
|
||||
}
|
||||
|
||||
$tz = new DateTimeZone('Europe/Paris');
|
||||
$currentPraticienName = trim(((string) ($prat['prenom'] ?? '')) . ' ' . ((string) ($prat['nom'] ?? '')));
|
||||
|
||||
$erreur = null;
|
||||
|
||||
// ---- Ajouter une plage datée (POST + CSRF)
|
||||
if (
|
||||
$_SERVER['REQUEST_METHOD'] === 'POST'
|
||||
&& ($_POST['action'] ?? '') === 'add'
|
||||
&& isset($_POST['date_dispo'], $_POST['heure_debut'], $_POST['heure_fin'])
|
||||
) {
|
||||
require_post_csrf();
|
||||
|
||||
$date = trim((string) $_POST['date_dispo']); // YYYY-MM-DD
|
||||
$debut = trim((string) $_POST['heure_debut']); // HH:MM
|
||||
$fin = trim((string) $_POST['heure_fin']); // HH:MM
|
||||
|
||||
$tz = new DateTimeZone('Europe/Paris');
|
||||
$today = (new DateTimeImmutable('now', $tz))->format('Y-m-d');
|
||||
|
||||
if (!preg_match('#^\d{4}-\d{2}-\d{2}$#', $date)) {
|
||||
if (! preg_match('#^\d{4}-\d{2}-\d{2}$#', $date)) {
|
||||
$erreur = $translations['erreur_format_date'] ?? "Format de date invalide.";
|
||||
} elseif (!preg_match('#^\d{2}:\d{2}$#', $debut) || !preg_match('#^\d{2}:\d{2}$#', $fin)) {
|
||||
} elseif (! preg_match('#^\d{2}:\d{2}$#', $debut) || ! preg_match('#^\d{2}:\d{2}$#', $fin)) {
|
||||
$erreur = $translations['erreur_format_heure'] ?? "Format d’heure invalide.";
|
||||
} elseif ($date < $today) {
|
||||
$erreur = $translations['erreur_date_passee'] ?? "Date dans le passé.";
|
||||
@@ -54,175 +80,222 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['date_dispo'], $_POST[
|
||||
$erreur = $translations['erreur_debut_avant_fin'] ?? "Heure de début après l’heure de fin.";
|
||||
}
|
||||
|
||||
if (!$erreur) {
|
||||
if (! $erreur) {
|
||||
try {
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO disponibilite_date (praticien_id, date_dispo, heure_debut, heure_fin)
|
||||
VALUES (:p, :d, :hd, :hf)
|
||||
INSERT INTO disponibilite_date (praticien_id, date_dispo, heure_debut, heure_fin)
|
||||
VALUES (:p, :d, :hd, :hf)
|
||||
");
|
||||
$stmt->execute([
|
||||
'p' => $praticien_id,
|
||||
'd' => $date,
|
||||
'hd' => $debut . ':00',
|
||||
'hf' => $fin . ':00',
|
||||
'p' => $praticien_id,
|
||||
'd' => $date,
|
||||
'hd' => $debut . ':00',
|
||||
'hf' => $fin . ':00',
|
||||
]);
|
||||
|
||||
header('Location: /public/cabinet/disponibilites_date.php?praticien_id='.$praticien_id.'&ok=added', true, 303);
|
||||
header('Location: /public/cabinet/disponibilites_date.php?praticien_id=' . $praticien_id . '&ok=added', true, 303);
|
||||
exit;
|
||||
} catch (PDOException $e) {
|
||||
$erreur = $translations['erreur_enregistrement'] ?? "Impossible d’enregistrer (doublon ou erreur).";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Supprimer une plage datée (GET)
|
||||
if (isset($_GET['supprimer'])) {
|
||||
$id = (int)$_GET['supprimer'];
|
||||
|
||||
// Vérifier appartenance
|
||||
$own = $pdo->prepare("SELECT 1 FROM disponibilite_date WHERE id = :id AND praticien_id = :p LIMIT 1");
|
||||
$own->execute(['id'=>$id,'p'=>$praticien_id]);
|
||||
|
||||
if ($own->fetchColumn()) {
|
||||
$stmt = $pdo->prepare("DELETE FROM disponibilite_date WHERE id = :id AND praticien_id = :p");
|
||||
$stmt->execute(['id'=>$id,'p'=>$praticien_id]);
|
||||
header('Location: /public/cabinet/disponibilites_date.php?praticien_id='.$praticien_id.'&ok=deleted', true, 303);
|
||||
exit;
|
||||
} else {
|
||||
http_response_code(403); exit('Forbidden');
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Liste
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT id, date_dispo, heure_debut, heure_fin
|
||||
// ---- Liste
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT id, date_dispo, heure_debut, heure_fin
|
||||
FROM disponibilite_date
|
||||
WHERE praticien_id = ?
|
||||
ORDER BY date_dispo ASC, heure_debut ASC
|
||||
WHERE praticien_id = ?
|
||||
ORDER BY date_dispo ASC, heure_debut ASC
|
||||
");
|
||||
$stmt->execute([$praticien_id]);
|
||||
$dates = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
$stmt->execute([$praticien_id]);
|
||||
$dates = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Header cabinet
|
||||
|
||||
|
||||
|
||||
$PAGE_TITRE = $translations['titre_disponibilites_date'] ?? 'Disponibilités (dates)';
|
||||
$HEAD_EXTRA = <<<HTML
|
||||
<script src="/js/sweetalert2@11.js"></script>
|
||||
$PAGE_TITLE = $translations['titre_disponibilites_date'] ?? 'Disponibilités (dates)';
|
||||
$HEAD_EXTRA = <<<HTML
|
||||
<script src="/js/sweetalert2@11.js"></script>
|
||||
HTML;
|
||||
require_once __DIR__ . '/../includes/header_cabinet.php';
|
||||
|
||||
require_once __DIR__ . '/../includes/header_cabinet.php';
|
||||
?>
|
||||
|
||||
<div class="main1">
|
||||
<div class="container py-5">
|
||||
<div class="shadow p-4 rounded bg-light">
|
||||
<h2 class="mb-4 text-center">
|
||||
<?= h($currentPraticienName ?: ($translations['h2_disponibilites_date'] ?? 'Disponibilités ponctuelles')) ?>
|
||||
<?php if ($currentPraticienName): ?>
|
||||
- <?= h($translations['h2_disponibilites_date'] ?? 'Disponibilités ponctuelles') ?>
|
||||
<?php endif; ?>
|
||||
</h2>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<a class="btn btn-outline-secondary"
|
||||
href="/public/cabinet/disponibilites.php?praticien_id=<?= (int)$praticien_id ?>">
|
||||
<?= h($translations['retour_au_planning'] ?? 'Retour') ?>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive mb-4">
|
||||
<table class="table table-bordered table-striped align-middle text-center mobile-cards" id="table_dispo_date">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th><?= h($translations['cl_date_rdv'] ?? 'Date') ?></th>
|
||||
<th><?= h($translations['table_debut'] ?? 'Début') ?></th>
|
||||
<th><?= h($translations['table_fin'] ?? 'Fin') ?></th>
|
||||
<th><?= h($translations['table_action'] ?? 'Action') ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if ($dates): foreach ($dates as $d): ?>
|
||||
<tr>
|
||||
<td><?= h(date('d/m/Y', strtotime((string)$d['date_dispo']))) ?></td>
|
||||
<td><?= h(substr((string)$d['heure_debut'], 0, 5)) ?></td>
|
||||
<td><?= h(substr((string)$d['heure_fin'], 0, 5)) ?></td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-sm btn-danger js-del" data-id="<?= (int)$d['id'] ?>">
|
||||
<?= h($translations['btn_supprimer'] ?? 'Supprimer') ?>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; else: ?>
|
||||
<tr><td colspan="4"><?= h($translations['aucun_creneau'] ?? 'Aucun créneau.') ?></td></tr>
|
||||
<div class="container py-5">
|
||||
<div class="shadow p-4 rounded bg-light">
|
||||
<h2 class="mb-4 text-center">
|
||||
<?php echo h($currentPraticienName ?: ($translations['h2_disponibilites_date'] ?? 'Disponibilités ponctuelles')) ?>
|
||||
<?php if ($currentPraticienName): ?>
|
||||
- <?php echo h($translations['h2_disponibilites_date'] ?? 'Disponibilités ponctuelles') ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</h2>
|
||||
|
||||
<div class="card shadow-sm p-4">
|
||||
<h4 class="mb-3 text-center"><?= h($translations['card_ajouter_creneau_date'] ?? 'Ajouter une disponibilité ponctuelle') ?></h4>
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<a class="btn btn-outline-secondary"
|
||||
href="/public/cabinet/disponibilites.php?praticien_id=<?php echo (int) $praticien_id ?>">
|
||||
<?php echo h($translations['retour_au_planning'] ?? 'Retour') ?>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<form method="POST">
|
||||
<input type="hidden" name="praticien_id" value="<?= (int)$praticien_id ?>">
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="date_dispo"><?= h($translations['cl_date_rdv'] ?? 'Date') ?></label>
|
||||
<input type="date" name="date_dispo" id="date_dispo" class="form-control" required>
|
||||
</div>
|
||||
<div class="table-responsive mb-4">
|
||||
<table class="table table-bordered table-striped align-middle text-center mobile-cards" id="table_dispo_date">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th><?php echo h($translations['cl_date_rdv'] ?? 'Date') ?></th>
|
||||
<th><?php echo h($translations['table_debut'] ?? 'Début') ?></th>
|
||||
<th><?php echo h($translations['table_fin'] ?? 'Fin') ?></th>
|
||||
<th><?php echo h($translations['table_action'] ?? 'Action') ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if ($dates): ?>
|
||||
<?php foreach ($dates as $d): ?>
|
||||
<tr>
|
||||
<td><?php echo h(date('d/m/Y', strtotime((string) $d['date_dispo']))) ?></td>
|
||||
<td><?php echo h(substr((string) $d['heure_debut'], 0, 5)) ?></td>
|
||||
<td><?php echo h(substr((string) $d['heure_fin'], 0, 5)) ?></td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-sm btn-danger js-del" data-id="<?php echo (int) $d['id'] ?>">
|
||||
<?php echo h($translations['btn_supprimer'] ?? 'Supprimer') ?>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr>
|
||||
<td colspan="4"><?php echo h($translations['aucun_creneau'] ?? 'Aucun créneau.') ?></td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="heure_debut"><?= h($translations['label_heure_debut'] ?? 'Heure de début') ?></label>
|
||||
<input type="time" name="heure_debut" id="heure_debut" class="form-control" required>
|
||||
</div>
|
||||
<div class="card shadow-sm p-4">
|
||||
<h4 class="mb-3 text-center"><?php echo h($translations['card_ajouter_creneau_date'] ?? 'Ajouter une disponibilité ponctuelle') ?></h4>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="heure_fin"><?= h($translations['label_heure_fin'] ?? 'Heure de fin') ?></label>
|
||||
<input type="time" name="heure_fin" id="heure_fin" class="form-control" required>
|
||||
</div>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="action" value="add">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo h(ensure_csrf_token()) ?>">
|
||||
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
<?= h($translations['btn_ajouter'] ?? 'Ajouter') ?>
|
||||
</button>
|
||||
</form>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="date_dispo"><?php echo h($translations['cl_date_rdv'] ?? 'Date') ?></label>
|
||||
<input type="date" name="date_dispo" id="date_dispo" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="heure_debut"><?php echo h($translations['label_heure_debut'] ?? 'Heure de début') ?></label>
|
||||
<input type="time" name="heure_debut" id="heure_debut" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="heure_fin"><?php echo h($translations['label_heure_fin'] ?? 'Heure de fin') ?></label>
|
||||
<input type="time" name="heure_fin" id="heure_fin" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
<?php echo h($translations['btn_ajouter'] ?? 'Ajouter') ?>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const csrfToken = <?php echo json_encode(ensure_csrf_token(), JSON_UNESCAPED_UNICODE) ?>;
|
||||
const praticienId = <?php echo (int) $praticien_id ?>;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function(){
|
||||
const okParam = '<?= isset($_GET['ok']) ? addslashes((string)$_GET['ok']) : '' ?>';
|
||||
const erreurMsg = '<?= $erreur ? addslashes((string)$erreur) : '' ?>';
|
||||
const okParam = <?php echo json_encode(isset($_GET['ok']) ? (string) $_GET['ok'] : '', JSON_UNESCAPED_UNICODE) ?>;
|
||||
const erreurMsg = <?php echo json_encode($erreur ?: '', JSON_UNESCAPED_UNICODE) ?>;
|
||||
|
||||
if (okParam === 'added') {
|
||||
Swal.fire({ icon:'success', title:'<?= addslashes($translations["msg_creneau_ajoute"] ?? "Créneau ajouté.") ?>', timer:1800, showConfirmButton:false });
|
||||
} else if (okParam === 'deleted') {
|
||||
Swal.fire({ icon:'success', title:'<?= addslashes($translations["msg_creneau_supprime"] ?? "Créneau supprimé.") ?>', timer:1800, showConfirmButton:false });
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: <?php echo json_encode($translations["msg_creneau_ajoute"] ?? "Créneau ajouté.", JSON_UNESCAPED_UNICODE) ?>,
|
||||
timer: 1800,
|
||||
showConfirmButton: false
|
||||
});
|
||||
|
||||
if (window.history.replaceState) {
|
||||
window.history.replaceState({}, document.title, window.location.pathname + '?praticien_id=' + encodeURIComponent(praticienId));
|
||||
}
|
||||
}
|
||||
|
||||
if (erreurMsg) {
|
||||
Swal.fire({ icon:'error', title:'<?= addslashes($translations["erreur_titre"] ?? "Erreur") ?>', text:erreurMsg, confirmButtonText:'OK' });
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: <?php echo json_encode($translations["erreur_titre"] ?? "Erreur", JSON_UNESCAPED_UNICODE) ?>,
|
||||
text: erreurMsg,
|
||||
confirmButtonText: 'OK'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', async function(e){
|
||||
const btn = e.target.closest('.js-del');
|
||||
if (!btn) return;
|
||||
|
||||
const id = btn.dataset.id;
|
||||
if (!id) return;
|
||||
|
||||
const result = await Swal.fire({
|
||||
icon: 'warning',
|
||||
title: '<?= addslashes($translations["confirm_title"] ?? "Confirmation") ?>',
|
||||
text: '<?= addslashes($translations["confirm_supprimer_creneau"] ?? "Supprimer ce créneau ?") ?>',
|
||||
title: <?php echo json_encode($translations["confirm_title"] ?? "Confirmation", JSON_UNESCAPED_UNICODE) ?>,
|
||||
text: <?php echo json_encode($translations["confirm_supprimer_creneau"] ?? "Supprimer ce créneau ?", JSON_UNESCAPED_UNICODE) ?>,
|
||||
showCancelButton: true,
|
||||
confirmButtonText: '<?= addslashes($translations["btn_da_udalit"] ?? "Oui") ?>',
|
||||
cancelButtonText: '<?= addslashes($translations["btn_annuler"] ?? "Annuler") ?>',
|
||||
confirmButtonText: <?php echo json_encode($translations["btn_da_udalit"] ?? "Oui", JSON_UNESCAPED_UNICODE) ?>,
|
||||
cancelButtonText: <?php echo json_encode($translations["btn_annuler"] ?? "Annuler", JSON_UNESCAPED_UNICODE) ?>,
|
||||
reverseButtons: true
|
||||
});
|
||||
|
||||
if (result.isConfirmed) {
|
||||
const pid = '<?= (int)$praticien_id ?>';
|
||||
window.location.href = '?praticien_id=' + encodeURIComponent(pid) + '&supprimer=' + encodeURIComponent(id);
|
||||
}
|
||||
if (!result.isConfirmed) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('id', String(id));
|
||||
formData.append('praticien_id', String(praticienId));
|
||||
formData.append('csrf_token', csrfToken);
|
||||
|
||||
fetch('/public/cabinet/supprimer_disponibilite_date.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: formData
|
||||
})
|
||||
.then(async r => {
|
||||
const text = await r.text();
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch (e) {
|
||||
return { ok: false, message: text };
|
||||
}
|
||||
})
|
||||
.then(data => {
|
||||
if (data.ok) {
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: <?php echo json_encode($translations["msg_creneau_supprime"] ?? "Créneau supprimé.", JSON_UNESCAPED_UNICODE) ?>,
|
||||
timer: 1400,
|
||||
showConfirmButton: false
|
||||
}).then(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
} else {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: <?php echo json_encode($translations["erreur_titre"] ?? "Erreur", JSON_UNESCAPED_UNICODE) ?>,
|
||||
text: data.message || 'Suppression impossible.'
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'Erreur réseau',
|
||||
text: 'Veuillez réessayer.'
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -238,15 +311,20 @@ document.addEventListener('click', async function(e){
|
||||
});
|
||||
document.documentElement.classList.add('mc-ready');
|
||||
}
|
||||
function ready(fn){ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fn); else fn(); }
|
||||
ready(function(){ initMobileCards(document.getElementById('table_dispo_date')); });
|
||||
function ready(fn){
|
||||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fn);
|
||||
else fn();
|
||||
}
|
||||
ready(function(){
|
||||
initMobileCards(document.getElementById('table_dispo_date'));
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p><?= $translations['copyr'] ?? '© '.date('Y') ?></p>
|
||||
<p><?php echo $translations['copyr'] ?? '© ' . date('Y') ?></p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
|
||||
require_once __DIR__ . '/../includes/init_secretaire.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$compteId = (int) ($_SESSION['compte_id'] ?? 0);
|
||||
$userRole = strtolower((string) ($_SESSION['role'] ?? ''));
|
||||
$praticien_id = (int) ($_POST['praticien_id'] ?? 0);
|
||||
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'message' => 'Méthode non autorisée.',
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
require_post_csrf();
|
||||
|
||||
if ($praticien_id <= 0 || $userRole !== 'secretaire') {
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'message' => 'Accès refusé.',
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Vérifier pivot + droit can_edit_slots
|
||||
$st = $pdo->prepare("
|
||||
SELECT can_edit_slots
|
||||
FROM secretaire_praticien
|
||||
WHERE secretaire_user_id = :u
|
||||
AND praticien_id = :p
|
||||
AND is_active = 1
|
||||
LIMIT 1
|
||||
");
|
||||
$st->execute([
|
||||
'u' => $compteId,
|
||||
'p' => $praticien_id,
|
||||
]);
|
||||
$pivot = $st->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (! $pivot || (int) $pivot['can_edit_slots'] !== 1) {
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'message' => 'Permission insuffisante.',
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
$id = (int) ($_POST['id'] ?? 0);
|
||||
if ($id <= 0) {
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'message' => 'ID invalide.',
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$check = $pdo->prepare("
|
||||
SELECT id, praticien_id
|
||||
FROM disponibilite
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
");
|
||||
$check->execute([$id]);
|
||||
$slot = $check->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (! $slot) {
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'message' => 'Créneau introuvable.',
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ((int) $slot['praticien_id'] !== $praticien_id) {
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'message' => 'Ce créneau ne correspond pas au praticien sélectionné.',
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("DELETE FROM disponibilite WHERE id = ? AND praticien_id = ?");
|
||||
$stmt->execute([$id, $praticien_id]);
|
||||
|
||||
echo json_encode([
|
||||
'ok' => ($stmt->rowCount() > 0),
|
||||
'message' => $stmt->rowCount() > 0
|
||||
? 'Créneau supprimé.'
|
||||
: 'Aucune ligne supprimée.',
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
|
||||
} catch (Throwable $e) {
|
||||
error_log('cabinet/supprimer_disponibilite.php: ' . $e->getMessage());
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'message' => 'Erreur serveur.',
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
|
||||
require_once __DIR__ . '/../includes/init_secretaire.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$compteId = (int) ($_SESSION['compte_id'] ?? 0);
|
||||
$userRole = strtolower((string) ($_SESSION['role'] ?? ''));
|
||||
$praticien_id = (int) ($_POST['praticien_id'] ?? 0);
|
||||
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'message' => 'Méthode non autorisée.',
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
require_post_csrf();
|
||||
|
||||
if ($praticien_id <= 0 || $userRole !== 'secretaire') {
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'message' => 'Accès refusé.',
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Vérifier pivot + droit can_edit_slots
|
||||
$st = $pdo->prepare("
|
||||
SELECT can_edit_slots
|
||||
FROM secretaire_praticien
|
||||
WHERE secretaire_user_id = :u
|
||||
AND praticien_id = :p
|
||||
AND is_active = 1
|
||||
LIMIT 1
|
||||
");
|
||||
$st->execute([
|
||||
'u' => $compteId,
|
||||
'p' => $praticien_id,
|
||||
]);
|
||||
$pivot = $st->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (! $pivot || (int) $pivot['can_edit_slots'] !== 1) {
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'message' => 'Permission insuffisante.',
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
$id = (int) ($_POST['id'] ?? 0);
|
||||
if ($id <= 0) {
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'message' => 'ID invalide.',
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$check = $pdo->prepare("
|
||||
SELECT id, praticien_id
|
||||
FROM disponibilite_date
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
");
|
||||
$check->execute([$id]);
|
||||
$slot = $check->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (! $slot) {
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'message' => 'Créneau introuvable.',
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ((int) $slot['praticien_id'] !== $praticien_id) {
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'message' => 'Ce créneau ne correspond pas au praticien sélectionné.',
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("DELETE FROM disponibilite_date WHERE id = ? AND praticien_id = ?");
|
||||
$stmt->execute([$id, $praticien_id]);
|
||||
|
||||
echo json_encode([
|
||||
'ok' => ($stmt->rowCount() > 0),
|
||||
'message' => $stmt->rowCount() > 0
|
||||
? 'Créneau supprimé.'
|
||||
: 'Aucune ligne supprimée.',
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
|
||||
} catch (Throwable $e) {
|
||||
error_log('cabinet/supprimer_disponibilite_date.php: ' . $e->getMessage());
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'message' => 'Erreur serveur.',
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
+19
-5
@@ -1,11 +1,25 @@
|
||||
<?php
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
declare (strict_types = 1);
|
||||
|
||||
if (isset($_GET['lang']) && in_array($_GET['lang'], ['ru', 'fr'])) {
|
||||
require_once __DIR__ . '/../../includes/bootstrap_global.php';
|
||||
|
||||
if (isset($_GET['lang']) && in_array($_GET['lang'], ['ru', 'fr'], true)) {
|
||||
$_SESSION['lang'] = $_GET['lang'];
|
||||
}
|
||||
|
||||
header('Location: ' . ($_SERVER['HTTP_REFERER'] ?? '/'));
|
||||
$back = '/';
|
||||
|
||||
if (! empty($_SERVER['HTTP_REFERER'])) {
|
||||
$refererPath = parse_url((string) $_SERVER['HTTP_REFERER'], PHP_URL_PATH);
|
||||
$refererQuery = parse_url((string) $_SERVER['HTTP_REFERER'], PHP_URL_QUERY);
|
||||
|
||||
if (is_string($refererPath) && preg_match('#^/#', $refererPath)) {
|
||||
$back = $refererPath;
|
||||
if (is_string($refererQuery) && $refererQuery !== '') {
|
||||
$back .= '?' . $refererQuery;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
header('Location: ' . $back, true, 303);
|
||||
exit;
|
||||
@@ -1,27 +1,88 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
|
||||
require_once __DIR__ . '/../includes/init_client.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
|
||||
|
||||
$client_id = $_SESSION['reference_id'];
|
||||
$praticien_id = intval($_POST['praticien_id'] ?? 0);
|
||||
|
||||
if ($praticien_id > 0) {
|
||||
|
||||
|
||||
// Évite les doublons
|
||||
$check = $pdo->prepare("SELECT * FROM praticien_client WHERE client_id = ? AND praticien_id = ?");
|
||||
$check->execute([$client_id, $praticien_id]);
|
||||
|
||||
if (!$check->fetch()) {
|
||||
$stmt = $pdo->prepare("INSERT INTO praticien_client (client_id, praticien_id) VALUES (?, ?)");
|
||||
$stmt->execute([$client_id, $praticien_id]);
|
||||
echo $translations['praticien_ajoute_favoris'];
|
||||
} else {
|
||||
echo $translations['praticien_deja_ajoute'];
|
||||
}
|
||||
} else {
|
||||
echo $translations['praticien_id_invalide'];
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'message' => 'Méthode non autorisée.',
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
require_post_csrf();
|
||||
|
||||
$client_id = (int) ($_SESSION['reference_id'] ?? 0);
|
||||
$praticien_id = (int) ($_POST['praticien_id'] ?? 0);
|
||||
|
||||
if ($client_id <= 0) {
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'message' => $translations['erreur_generique'] ?? 'Erreur.',
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($praticien_id <= 0) {
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'message' => $translations['praticien_id_invalide'] ?? 'ID praticien invalide.',
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
// Vérifie que le praticien existe
|
||||
$exists = $pdo->prepare("SELECT id FROM praticien WHERE id = ? LIMIT 1");
|
||||
$exists->execute([$praticien_id]);
|
||||
|
||||
if (! $exists->fetchColumn()) {
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'message' => $translations['praticien_id_invalide'] ?? 'ID praticien invalide.',
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Évite les doublons
|
||||
$check = $pdo->prepare("
|
||||
SELECT 1
|
||||
FROM praticien_client
|
||||
WHERE client_id = ? AND praticien_id = ?
|
||||
LIMIT 1
|
||||
");
|
||||
$check->execute([$client_id, $praticien_id]);
|
||||
|
||||
if ($check->fetchColumn()) {
|
||||
echo json_encode([
|
||||
'ok' => true,
|
||||
'already_exists' => true,
|
||||
'message' => $translations['praticien_deja_ajoute'] ?? 'Déjà ajouté aux favoris.',
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO praticien_client (client_id, praticien_id)
|
||||
VALUES (?, ?)
|
||||
");
|
||||
$stmt->execute([$client_id, $praticien_id]);
|
||||
|
||||
echo json_encode([
|
||||
'ok' => true,
|
||||
'already_exists' => false,
|
||||
'message' => $translations['praticien_ajoute_favoris'] ?? 'Praticien ajouté aux favoris.',
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
|
||||
} catch (Throwable $e) {
|
||||
error_log('ajouter_favorit.php: ' . $e->getMessage());
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'message' => $translations['erreur_generique'] ?? 'Erreur serveur.',
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
+347
-318
@@ -1,310 +1,333 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../includes/init_client.php';
|
||||
require_once __DIR__ . '/../../push/push_endpoint.php';
|
||||
|
||||
require_post_csrf();
|
||||
|
||||
// PHPMailer (config centralisée)
|
||||
|
||||
|
||||
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use PHPMailer\PHPMailer\Exception;
|
||||
|
||||
// --------- Réponse JSON utilitaire ----------
|
||||
function json_out(bool $ok, string $msg, array $extra = []): never {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode(['ok'=>$ok, 'message'=>$msg] + $extra);
|
||||
exit;
|
||||
function json_out(bool $ok, string $msg, array $extra = []): never
|
||||
{
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode(['ok' => $ok, 'message' => $msg] + $extra);
|
||||
exit;
|
||||
}
|
||||
|
||||
// --------- Envoi e-mail via PHPMailer (avec ICS optionnel + fallback) ----------
|
||||
function send_email_phpmailer(
|
||||
string $toEmail,
|
||||
string $subject,
|
||||
string $textBody,
|
||||
string $htmlBody,
|
||||
?string $icsFilename = null,
|
||||
?string $icsContent = null
|
||||
string $toEmail,
|
||||
string $subject,
|
||||
string $textBody,
|
||||
string $htmlBody,
|
||||
?string $icsFilename = null,
|
||||
?string $icsContent = null
|
||||
): array {
|
||||
global $MAILCFG;
|
||||
global $MAILCFG;
|
||||
|
||||
$mail = new PHPMailer(true);
|
||||
try {
|
||||
// SMTP
|
||||
$mail->isSMTP();
|
||||
$mail->Host = $MAILCFG['smtp']['host'];
|
||||
$mail->Port = $MAILCFG['smtp']['port'];
|
||||
$mail->SMTPSecure = $MAILCFG['smtp']['secure'];
|
||||
$mail->SMTPAuth = $MAILCFG['smtp']['auth'];
|
||||
$mail->Username = $MAILCFG['smtp']['username'];
|
||||
$mail->Password = $MAILCFG['smtp']['password'];
|
||||
|
||||
$mail->CharSet = 'UTF-8';
|
||||
$mail->Encoding = 'base64';
|
||||
$mail->XMailer = '';
|
||||
|
||||
// Debug ponctuel : ?debug_mail=1
|
||||
if (!empty($_GET['debug_mail'])) {
|
||||
$mail->SMTPDebug = 2;
|
||||
$mail->Debugoutput = function($str,$lvl){ error_log("SMTP[$lvl] ".$str); };
|
||||
}
|
||||
|
||||
if (!empty($MAILCFG['bounce'])) $mail->Sender = $MAILCFG['bounce'];
|
||||
|
||||
$from = $MAILCFG['from'] ?? ['no-reply@uslugi.fr','USLUGI.FR'];
|
||||
$rpto = $MAILCFG['reply_to'] ?? ['contact@uslugi.fr','USLUGI.FR'];
|
||||
$mail->setFrom($from[0], $from[1] ?? '');
|
||||
if (!empty($rpto[0])) $mail->addReplyTo($rpto[0], $rpto[1] ?? '');
|
||||
|
||||
$mail->addAddress($toEmail);
|
||||
|
||||
if (!empty($MAILCFG['suppress_auto'])) {
|
||||
$mail->addCustomHeader('Auto-Submitted', 'auto-generated');
|
||||
$mail->addCustomHeader('X-Auto-Response-Suppress', 'All');
|
||||
}
|
||||
|
||||
$mail->isHTML(true);
|
||||
$mail->Subject = $subject;
|
||||
$mail->Body = $htmlBody;
|
||||
$mail->AltBody = $textBody;
|
||||
|
||||
if ($icsFilename && $icsContent !== null && empty($_GET['no_ics'])) {
|
||||
$mail->addStringAttachment(
|
||||
$icsContent,
|
||||
$icsFilename,
|
||||
'base64',
|
||||
'text/calendar; method=CANCEL; charset=UTF-8'
|
||||
);
|
||||
}
|
||||
|
||||
$mail->send();
|
||||
return [true, null];
|
||||
|
||||
} catch (Throwable $e) {
|
||||
$err = 'PHPMailer error: '.$e->getMessage();
|
||||
if (!empty($mail->ErrorInfo)) $err .= ' | ErrorInfo: '.$mail->ErrorInfo;
|
||||
error_log($err);
|
||||
|
||||
// Fallback : réessaye SANS ICS
|
||||
$mail = new PHPMailer(true);
|
||||
try {
|
||||
if ($icsFilename && $icsContent !== null && empty($_GET['no_ics'])) {
|
||||
$mail->clearAttachments();
|
||||
// SMTP
|
||||
$mail->isSMTP();
|
||||
$mail->Host = $MAILCFG['smtp']['host'];
|
||||
$mail->Port = $MAILCFG['smtp']['port'];
|
||||
$mail->SMTPSecure = $MAILCFG['smtp']['secure'];
|
||||
$mail->SMTPAuth = $MAILCFG['smtp']['auth'];
|
||||
$mail->Username = $MAILCFG['smtp']['username'];
|
||||
$mail->Password = $MAILCFG['smtp']['password'];
|
||||
|
||||
$mail->CharSet = 'UTF-8';
|
||||
$mail->Encoding = 'base64';
|
||||
$mail->XMailer = '';
|
||||
|
||||
// Debug ponctuel : ?debug_mail=1
|
||||
if (! empty($_GET['debug_mail'])) {
|
||||
$mail->SMTPDebug = 2;
|
||||
$mail->Debugoutput = function ($str, $lvl) {error_log("SMTP[$lvl] " . $str);};
|
||||
}
|
||||
|
||||
if (! empty($MAILCFG['bounce'])) {
|
||||
$mail->Sender = $MAILCFG['bounce'];
|
||||
}
|
||||
|
||||
$from = $MAILCFG['from'] ?? ['no-reply@uslugi.fr', 'USLUGI.FR'];
|
||||
$rpto = $MAILCFG['reply_to'] ?? ['contact@uslugi.fr', 'USLUGI.FR'];
|
||||
$mail->setFrom($from[0], $from[1] ?? '');
|
||||
if (! empty($rpto[0])) {
|
||||
$mail->addReplyTo($rpto[0], $rpto[1] ?? '');
|
||||
}
|
||||
|
||||
$mail->addAddress($toEmail);
|
||||
|
||||
if (! empty($MAILCFG['suppress_auto'])) {
|
||||
$mail->addCustomHeader('Auto-Submitted', 'auto-generated');
|
||||
$mail->addCustomHeader('X-Auto-Response-Suppress', 'All');
|
||||
}
|
||||
|
||||
$mail->isHTML(true);
|
||||
$mail->Subject = $subject;
|
||||
$mail->Body = $htmlBody;
|
||||
$mail->AltBody = $textBody;
|
||||
|
||||
if ($icsFilename && $icsContent !== null && empty($_GET['no_ics'])) {
|
||||
$mail->addStringAttachment(
|
||||
$icsContent,
|
||||
$icsFilename,
|
||||
'base64',
|
||||
'text/calendar; method=CANCEL; charset=UTF-8'
|
||||
);
|
||||
}
|
||||
|
||||
$mail->send();
|
||||
return [true, 'sent_without_ics'];
|
||||
}
|
||||
} catch (Throwable $e2) {
|
||||
$err2 = 'PHPMailer fallback error: '.$e2->getMessage();
|
||||
if (!empty($mail->ErrorInfo)) $err2 .= ' | ErrorInfo: '.$mail->ErrorInfo;
|
||||
error_log($err2);
|
||||
return [true, null];
|
||||
|
||||
} catch (Throwable $e) {
|
||||
$err = 'PHPMailer error: ' . $e->getMessage();
|
||||
if (! empty($mail->ErrorInfo)) {
|
||||
$err .= ' | ErrorInfo: ' . $mail->ErrorInfo;
|
||||
}
|
||||
|
||||
error_log($err);
|
||||
|
||||
// Fallback : réessaye SANS ICS
|
||||
try {
|
||||
if ($icsFilename && $icsContent !== null && empty($_GET['no_ics'])) {
|
||||
$mail->clearAttachments();
|
||||
$mail->send();
|
||||
return [true, 'sent_without_ics'];
|
||||
}
|
||||
} catch (Throwable $e2) {
|
||||
$err2 = 'PHPMailer fallback error: ' . $e2->getMessage();
|
||||
if (! empty($mail->ErrorInfo)) {
|
||||
$err2 .= ' | ErrorInfo: ' . $mail->ErrorInfo;
|
||||
}
|
||||
|
||||
error_log($err2);
|
||||
}
|
||||
return [false, $err];
|
||||
}
|
||||
return [false, $err];
|
||||
}
|
||||
}
|
||||
|
||||
// --------- Helpers e-mail (HTML/Texte) ----------
|
||||
function email_html_cancel(array $data, array $t): string {
|
||||
$brand = '<img src="https://uslugi.fr/public/uploads/img/logo_mail.png" width="150" alt="USLUGI.FR">';
|
||||
$brand_bas ='УСЛУГИ.ФР';
|
||||
return '<!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>USLUGI.FR</title></head>
|
||||
function email_html_cancel(array $data, array $t): string
|
||||
{
|
||||
$brand = '<img src="https://uslugi.fr/public/uploads/img/logo_mail.png" width="150" alt="USLUGI.FR">';
|
||||
$brand_bas = 'УСЛУГИ.ФР';
|
||||
return '<!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>USLUGI.FR</title></head>
|
||||
<body style="margin:0;padding:0;background:#f4f6f8;font-family:Arial,Helvetica,sans-serif;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f4f6f8;padding:24px 0;">
|
||||
<tr><td align="center">
|
||||
<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="background:#ffffff;border-radius:8px;overflow:hidden;">
|
||||
<tr><td style="background:#38378B;color:#ffffff;padding:20px 24px;font-size:18px;font-weight:600;">'.$brand.'</td></tr>
|
||||
<tr><td style="padding:24px 24px 8px 24px;color:#111;font-size:16px;line-height:1.5;">'.nl2br(htmlspecialchars($data['hello_line'])).'</td></tr>
|
||||
<tr><td style="padding:8px 24px;color:#111;font-size:16px;line-height:1.6;">'.nl2br(htmlspecialchars($data['intro'])).'</td></tr>
|
||||
<tr><td style="background:#38378B;color:#ffffff;padding:20px 24px;font-size:18px;font-weight:600;">' . $brand . '</td></tr>
|
||||
<tr><td style="padding:24px 24px 8px 24px;color:#111;font-size:16px;line-height:1.5;">' . nl2br(htmlspecialchars($data['hello_line'])) . '</td></tr>
|
||||
<tr><td style="padding:8px 24px;color:#111;font-size:16px;line-height:1.6;">' . nl2br(htmlspecialchars($data['intro'])) . '</td></tr>
|
||||
<tr><td style="padding:16px 24px;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border:1px solid #e5e7eb;border-radius:8px;">
|
||||
<tr><td style="padding:14px 16px;font-size:14px;color:#6b7280;border-bottom:1px solid #e5e7eb;">'.htmlspecialchars($data['who_label']).'</td><td style="padding:14px 16px;font-size:14px;color:#111;">'.htmlspecialchars($data['who_value']).'</td></tr>
|
||||
<tr><td style="padding:14px 16px;font-size:14px;color:#6b7280;">'.htmlspecialchars($data['date_label']).'</td><td style="padding:14px 16px;font-size:14px;color:#111;">'.htmlspecialchars($data['when']).'</td></tr>
|
||||
<tr><td style="padding:14px 16px;font-size:14px;color:#6b7280;">'.htmlspecialchars($data['statut_label']).'</td><td style="padding:14px 16px;font-size:14px;color:#111;">'.htmlspecialchars($data['statut_value']).'</td></tr>
|
||||
<tr><td style="padding:14px 16px;font-size:14px;color:#6b7280;border-bottom:1px solid #e5e7eb;">' . htmlspecialchars($data['who_label']) . '</td><td style="padding:14px 16px;font-size:14px;color:#111;">' . htmlspecialchars($data['who_value']) . '</td></tr>
|
||||
<tr><td style="padding:14px 16px;font-size:14px;color:#6b7280;">' . htmlspecialchars($data['date_label']) . '</td><td style="padding:14px 16px;font-size:14px;color:#111;">' . htmlspecialchars($data['when']) . '</td></tr>
|
||||
<tr><td style="padding:14px 16px;font-size:14px;color:#6b7280;">' . htmlspecialchars($data['statut_label']) . '</td><td style="padding:14px 16px;font-size:14px;color:#111;">' . htmlspecialchars($data['statut_value']) . '</td></tr>
|
||||
</table>
|
||||
</td></tr>'.
|
||||
(!empty($data['cta_url']) ? '<tr><td align="center" style="padding:8px 24px 24px 24px;"><a href="'.htmlspecialchars($data['cta_url']).'" target="_blank" style="display:inline-block;text-decoration:none;padding:12px 20px;border-radius:999px;background:#E03C3F;color:#fff;font-weight:600;">'.htmlspecialchars($data['cta_label']).'</a></td></tr>' : '').
|
||||
'<tr><td style="padding:8px 24px 24px 24px;color:#111;font-size:16px;line-height:1.6;">'.nl2br(htmlspecialchars($data['footer'])).'</td></tr>
|
||||
</td></tr>' .
|
||||
(! empty($data['cta_url']) ? '<tr><td align="center" style="padding:8px 24px 24px 24px;"><a href="' . htmlspecialchars($data['cta_url']) . '" target="_blank" style="display:inline-block;text-decoration:none;padding:12px 20px;border-radius:999px;background:#E03C3F;color:#fff;font-weight:600;">' . htmlspecialchars($data['cta_label']) . '</a></td></tr>' : '') .
|
||||
'<tr><td style="padding:8px 24px 24px 24px;color:#111;font-size:16px;line-height:1.6;">' . nl2br(htmlspecialchars($data['footer'])) . '</td></tr>
|
||||
</table>
|
||||
<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="margin-top:12px;"><tr><td style="text-align:center;color:#9ca3af;font-size:12px;padding:8px 0;">© '.date('Y').' '.$brand_bas.' — '.htmlspecialchars($t['merci_simple'] ?? 'Merci').'</td></tr></table>
|
||||
<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="margin-top:12px;"><tr><td style="text-align:center;color:#9ca3af;font-size:12px;padding:8px 0;">© ' . date('Y') . ' ' . $brand_bas . ' — ' . htmlspecialchars($t['merci_simple'] ?? 'Merci') . '</td></tr></table>
|
||||
</td></tr>
|
||||
</table></body></html>';
|
||||
}
|
||||
function email_text_cancel(array $data, array $t): string {
|
||||
$lines = [];
|
||||
$lines[] = $data['hello_line']; $lines[]='';
|
||||
$lines[] = $data['intro']; $lines[]='';
|
||||
$lines[] = $data['who_label'].': '.$data['who_value'];
|
||||
$lines[] = $data['date_label'].': '.$data['when'];
|
||||
$lines[] = $data['statut_label'].': '.$data['statut_value'];
|
||||
if (!empty($data['cta_url'])) { $lines[]=''; $lines[] = $data['cta_label'].': '.$data['cta_url']; }
|
||||
$lines[]=''; $lines[] = $data['footer'];
|
||||
return implode("\n", $lines);
|
||||
function email_text_cancel(array $data, array $t): string
|
||||
{
|
||||
$lines = [];
|
||||
$lines[] = $data['hello_line'];
|
||||
$lines[] = '';
|
||||
$lines[] = $data['intro'];
|
||||
$lines[] = '';
|
||||
$lines[] = $data['who_label'] . ': ' . $data['who_value'];
|
||||
$lines[] = $data['date_label'] . ': ' . $data['when'];
|
||||
$lines[] = $data['statut_label'] . ': ' . $data['statut_value'];
|
||||
if (! empty($data['cta_url'])) {$lines[] = '';
|
||||
$lines[] = $data['cta_label'] . ': ' . $data['cta_url'];}
|
||||
$lines[] = '';
|
||||
$lines[] = $data['footer'];
|
||||
return implode("\n", $lines);
|
||||
}
|
||||
|
||||
// --------- ICS CANCEL ----------
|
||||
function ics_escape(string $s): string {
|
||||
$s = str_replace("\\","\\\\",$s);
|
||||
$s = str_replace(";","\;",$s);
|
||||
$s = str_replace(",","\,",$s);
|
||||
return preg_replace("/\r\n|\r|\n/","\\n",$s);
|
||||
function ics_escape(string $s): string
|
||||
{
|
||||
$s = str_replace("\\", "\\\\", $s);
|
||||
$s = str_replace(";", "\;", $s);
|
||||
$s = str_replace(",", "\,", $s);
|
||||
return preg_replace("/\r\n|\r|\n/", "\\n", $s);
|
||||
}
|
||||
function build_ics_cancel(
|
||||
string $uid,
|
||||
DateTime $start,
|
||||
?DateTime $end,
|
||||
string $timezone,
|
||||
string $summary,
|
||||
string $organizerEmail,
|
||||
array $attendees // [['cn'=>..., 'email'=>...]]
|
||||
string $uid,
|
||||
DateTime $start,
|
||||
?DateTime $end,
|
||||
string $timezone,
|
||||
string $summary,
|
||||
string $organizerEmail,
|
||||
array $attendees // [['cn'=>..., 'email'=>...]]
|
||||
): string {
|
||||
$lines=[];
|
||||
$lines[]="BEGIN:VCALENDAR";
|
||||
$lines[]="VERSION:2.0";
|
||||
$lines[]="PRODID:-//УСЛУГИ.ФР//RDV//FR";
|
||||
$lines[]="METHOD:CANCEL";
|
||||
$lines[]="BEGIN:VEVENT";
|
||||
$lines[]="UID:".$uid;
|
||||
$lines[]="DTSTAMP:".gmdate('Ymd\THis\Z');
|
||||
$lines[]="STATUS:CANCELLED";
|
||||
$lines[]="SEQUENCE:2";
|
||||
$lines[]="SUMMARY:".ics_escape($summary);
|
||||
if ($start) $lines[]="DTSTART;TZID={$timezone}:".$start->format('Ymd\THis');
|
||||
if ($end) $lines[]="DTEND;TZID={$timezone}:".$end->format('Ymd\THis');
|
||||
$lines[]="ORGANIZER:mailto:".$organizerEmail;
|
||||
foreach ($attendees as $a) {
|
||||
if (empty($a['email'])) continue;
|
||||
$cn = ics_escape($a['cn'] ?? '');
|
||||
$lines[]="ATTENDEE;ROLE=REQ-PARTICIPANT;CN={$cn}:mailto:".$a['email'];
|
||||
}
|
||||
$lines[]="END:VEVENT";
|
||||
$lines[]="END:VCALENDAR";
|
||||
return implode("\r\n",$lines)."\r\n";
|
||||
$lines = [];
|
||||
$lines[] = "BEGIN:VCALENDAR";
|
||||
$lines[] = "VERSION:2.0";
|
||||
$lines[] = "PRODID:-//УСЛУГИ.ФР//RDV//FR";
|
||||
$lines[] = "METHOD:CANCEL";
|
||||
$lines[] = "BEGIN:VEVENT";
|
||||
$lines[] = "UID:" . $uid;
|
||||
$lines[] = "DTSTAMP:" . gmdate('Ymd\THis\Z');
|
||||
$lines[] = "STATUS:CANCELLED";
|
||||
$lines[] = "SEQUENCE:2";
|
||||
$lines[] = "SUMMARY:" . ics_escape($summary);
|
||||
if ($start) {
|
||||
$lines[] = "DTSTART;TZID={$timezone}:" . $start->format('Ymd\THis');
|
||||
}
|
||||
|
||||
if ($end) {
|
||||
$lines[] = "DTEND;TZID={$timezone}:" . $end->format('Ymd\THis');
|
||||
}
|
||||
|
||||
$lines[] = "ORGANIZER:mailto:" . $organizerEmail;
|
||||
foreach ($attendees as $a) {
|
||||
if (empty($a['email'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$cn = ics_escape($a['cn'] ?? '');
|
||||
$lines[] = "ATTENDEE;ROLE=REQ-PARTICIPANT;CN={$cn}:mailto:" . $a['email'];
|
||||
}
|
||||
$lines[] = "END:VEVENT";
|
||||
$lines[] = "END:VCALENDAR";
|
||||
return implode("\r\n", $lines) . "\r\n";
|
||||
}
|
||||
|
||||
// =================== LOGIQUE ===================
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
|
||||
json_out(false, 'Méthode invalide.');
|
||||
json_out(false, 'Méthode invalide.');
|
||||
}
|
||||
|
||||
$client_id = $_SESSION['reference_id'] ?? null;
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
if (!$client_id || $id <= 0) {
|
||||
json_out(false, $translations['erreur_generique'] ?? 'Erreur.');
|
||||
$id = (int) ($_POST['id'] ?? 0);
|
||||
if (! $client_id || $id <= 0) {
|
||||
json_out(false, $translations['erreur_generique'] ?? 'Erreur.');
|
||||
}
|
||||
|
||||
try {
|
||||
// =========================
|
||||
// SNAPSHOT AVANT ANNULATION
|
||||
// =========================
|
||||
$beforeStmt = $pdo->prepare("
|
||||
// SNAPSHOT AVANT ANNULATION
|
||||
// =========================
|
||||
$beforeStmt = $pdo->prepare("
|
||||
SELECT statut, date_rdv, duree_minutes, lieu, praticien_id, client_id
|
||||
FROM rendez_vous
|
||||
WHERE id = ? AND client_id = ?
|
||||
LIMIT 1
|
||||
");
|
||||
$beforeStmt->execute([$id, $client_id]);
|
||||
$beforeRow = $beforeStmt->fetch(PDO::FETCH_ASSOC);
|
||||
$beforeStmt->execute([$id, $client_id]);
|
||||
$beforeRow = $beforeStmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$beforeRow) {
|
||||
json_out(false, $translations['erreur_generique'] ?? 'Rendez-vous introuvable.');
|
||||
exit;
|
||||
}
|
||||
if (! $beforeRow) {
|
||||
json_out(false, $translations['erreur_generique'] ?? 'Rendez-vous introuvable.');
|
||||
exit;
|
||||
}
|
||||
// ✅ compte_id praticien + client (pour badge counters)
|
||||
$praticienCompteId = 0;
|
||||
$clientCompteId = 0;
|
||||
$praticienCompteId = 0;
|
||||
$clientCompteId = 0;
|
||||
|
||||
if (!empty($beforeRow['praticien_id'])) {
|
||||
$st = $pdo->prepare("SELECT id FROM compte WHERE role='praticien' AND reference_id=? LIMIT 1");
|
||||
$st->execute([(int)$beforeRow['praticien_id']]);
|
||||
$praticienCompteId = (int)($st->fetchColumn() ?: 0);
|
||||
}
|
||||
if (!empty($beforeRow['client_id'])) {
|
||||
$st = $pdo->prepare("SELECT id FROM compte WHERE role='client' AND reference_id=? LIMIT 1");
|
||||
$st->execute([(int)$beforeRow['client_id']]);
|
||||
$clientCompteId = (int)($st->fetchColumn() ?: 0);
|
||||
}
|
||||
if (! empty($beforeRow['praticien_id'])) {
|
||||
$st = $pdo->prepare("SELECT id FROM compte WHERE role='praticien' AND reference_id=? LIMIT 1");
|
||||
$st->execute([(int) $beforeRow['praticien_id']]);
|
||||
$praticienCompteId = (int) ($st->fetchColumn() ?: 0);
|
||||
}
|
||||
if (! empty($beforeRow['client_id'])) {
|
||||
$st = $pdo->prepare("SELECT id FROM compte WHERE role='client' AND reference_id=? LIMIT 1");
|
||||
$st->execute([(int) $beforeRow['client_id']]);
|
||||
$clientCompteId = (int) ($st->fetchColumn() ?: 0);
|
||||
}
|
||||
|
||||
// 1) Annuler (contrainte client_id)
|
||||
$stmt = $pdo->prepare("UPDATE rendez_vous SET statut = 'annulé' WHERE id = ? AND client_id = ?");
|
||||
$stmt->execute([$id, $client_id]);
|
||||
// 1) Annuler (contrainte client_id)
|
||||
$stmt = $pdo->prepare("UPDATE rendez_vous SET statut = 'annulé' WHERE id = ? AND client_id = ?");
|
||||
$stmt->execute([$id, $client_id]);
|
||||
|
||||
if ($stmt->rowCount() === 0) {
|
||||
// Rien modifié -> soit déjà annulé, soit pas à ce client, soit ID invalide
|
||||
json_out(false, $translations['aucune_modification'] ?? 'Aucune modification effectuée.');
|
||||
}
|
||||
if ($stmt->rowCount() === 0) {
|
||||
// Rien modifié -> soit déjà annulé, soit pas à ce client, soit ID invalide
|
||||
json_out(false, $translations['aucune_modification'] ?? 'Aucune modification effectuée.');
|
||||
}
|
||||
|
||||
// =========================
|
||||
// LOG RDV (rdv_actions_log) : cancel
|
||||
// =========================
|
||||
$beforeJson = json_encode([
|
||||
'statut' => $beforeRow['statut'],
|
||||
'date_rdv' => $beforeRow['date_rdv'],
|
||||
'duree_minutes' => (int)$beforeRow['duree_minutes'],
|
||||
'lieu' => $beforeRow['lieu'],
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
// =========================
|
||||
// LOG RDV (rdv_actions_log) : cancel
|
||||
// =========================
|
||||
$beforeJson = json_encode([
|
||||
'statut' => $beforeRow['statut'],
|
||||
'date_rdv' => $beforeRow['date_rdv'],
|
||||
'duree_minutes' => (int) $beforeRow['duree_minutes'],
|
||||
'lieu' => $beforeRow['lieu'],
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
$afterJson = json_encode([
|
||||
'statut' => 'annulé',
|
||||
'date_rdv' => $beforeRow['date_rdv'],
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
$afterJson = json_encode([
|
||||
'statut' => 'annulé',
|
||||
'date_rdv' => $beforeRow['date_rdv'],
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
$logStmt = $pdo->prepare("
|
||||
$logStmt = $pdo->prepare("
|
||||
INSERT INTO rdv_actions_log
|
||||
(rdv_id, action, by_user_id, by_role, before_json, after_json, created_at)
|
||||
VALUES
|
||||
(?, 'cancel', ?, 'client', ?, ?, NOW())
|
||||
");
|
||||
$logStmt->execute([(int)$id, (int)$client_id, $beforeJson, $afterJson]);
|
||||
// =========================
|
||||
$logStmt->execute([(int) $id, (int) $client_id, $beforeJson, $afterJson]);
|
||||
// =========================
|
||||
// PUSH RDV — annulation (client)
|
||||
// =========================
|
||||
try {
|
||||
// ✅ Badge RDV : +1 pour le praticien (cancel par client)
|
||||
if ($praticienCompteId > 0) {
|
||||
$pdo->prepare("
|
||||
try {
|
||||
// ✅ Badge RDV : +1 pour le praticien (cancel par client)
|
||||
if ($praticienCompteId > 0) {
|
||||
$pdo->prepare("
|
||||
INSERT INTO push_badge_counters (compte_id, msg_unread, rdv_unread)
|
||||
VALUES (:cid, 0, 1)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
rdv_unread = LEAST(99, rdv_unread + 1),
|
||||
updated_at = NOW()
|
||||
")->execute([':cid' => $praticienCompteId]);
|
||||
}
|
||||
}
|
||||
|
||||
// 🔔 Praticien : RDV annulé par le client
|
||||
push_rdv_to_praticien(
|
||||
$pdo,
|
||||
(int)$beforeRow['praticien_id'],
|
||||
$translations['push_rdv_cancel_praticien_title'] ?? 'Запись отменена',
|
||||
$translations['push_rdv_cancel_praticien_body'] ?? 'Клиент отменил запись.',
|
||||
'/public/praticien/rendez_vous.php?id='.(int)$id,
|
||||
[
|
||||
'rdv_id' => (int)$id,
|
||||
'action' => 'cancel',
|
||||
'from_role' => 'client',
|
||||
'client_id' => (int)$beforeRow['client_id'],
|
||||
'praticien_id' => (int)$beforeRow['praticien_id'],
|
||||
'date_rdv' => $beforeRow['date_rdv'],
|
||||
]
|
||||
);
|
||||
// 🔔 Praticien : RDV annulé par le client
|
||||
push_rdv_to_praticien(
|
||||
$pdo,
|
||||
(int) $beforeRow['praticien_id'],
|
||||
$translations['push_rdv_cancel_praticien_title'] ?? 'Запись отменена',
|
||||
$translations['push_rdv_cancel_praticien_body'] ?? 'Клиент отменил запись.',
|
||||
'/public/praticien/rendez_vous.php?id=' . (int) $id,
|
||||
[
|
||||
'rdv_id' => (int) $id,
|
||||
'action' => 'cancel',
|
||||
'from_role' => 'client',
|
||||
'client_id' => (int) $beforeRow['client_id'],
|
||||
'praticien_id' => (int) $beforeRow['praticien_id'],
|
||||
'date_rdv' => $beforeRow['date_rdv'],
|
||||
]
|
||||
);
|
||||
|
||||
// 🔔 Client : confirmation annulation
|
||||
push_rdv_to_client(
|
||||
$pdo,
|
||||
(int)$beforeRow['client_id'],
|
||||
$translations['push_rdv_cancel_client_title'] ?? 'Запись отменена',
|
||||
$translations['push_rdv_cancel_client_body'] ?? 'Ваша запись была успешно отменена.',
|
||||
'/public/client/mes_rendez_vous.php',
|
||||
[
|
||||
'rdv_id' => (int)$id,
|
||||
'action' => 'cancel'
|
||||
]
|
||||
);
|
||||
// 🔔 Client : confirmation annulation
|
||||
push_rdv_to_client(
|
||||
$pdo,
|
||||
(int) $beforeRow['client_id'],
|
||||
$translations['push_rdv_cancel_client_title'] ?? 'Запись отменена',
|
||||
$translations['push_rdv_cancel_client_body'] ?? 'Ваша запись была успешно отменена.',
|
||||
'/public/client/mes_rendez_vous.php',
|
||||
[
|
||||
'rdv_id' => (int) $id,
|
||||
'action' => 'cancel',
|
||||
]
|
||||
);
|
||||
|
||||
} catch (Throwable $e) {
|
||||
error_log('push rdv cancel (client) failed: '.$e->getMessage());
|
||||
}
|
||||
|
||||
} catch (Throwable $e) {
|
||||
error_log('push rdv cancel (client) failed: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
// 2) Infos RDV pour e-mails
|
||||
$info = $pdo->prepare("
|
||||
// 2) Infos RDV pour e-mails
|
||||
$info = $pdo->prepare("
|
||||
SELECT rv.date_rdv, rv.duree_minutes, rv.uuid_ical,
|
||||
p.email AS praticien_email, p.nom AS praticien_nom, p.prenom AS praticien_prenom,
|
||||
c.prenom AS client_prenom, c.nom AS client_nom, c.email AS client_email
|
||||
@@ -313,109 +336,115 @@ try {
|
||||
JOIN client c ON rv.client_id = c.id
|
||||
WHERE rv.id = ? AND rv.client_id = ?
|
||||
");
|
||||
$info->execute([$id, $client_id]);
|
||||
$r = $info->fetch(PDO::FETCH_ASSOC);
|
||||
$info->execute([$id, $client_id]);
|
||||
$r = $info->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$r) {
|
||||
json_out(false, $translations['erreur_generique'] ?? 'Erreur.');
|
||||
}
|
||||
if (! $r) {
|
||||
json_out(false, $translations['erreur_generique'] ?? 'Erreur.');
|
||||
}
|
||||
|
||||
// 3) Prépare mails
|
||||
$tz = new DateTimeZone('Europe/Paris');
|
||||
$start = new DateTime($r['date_rdv'], $tz);
|
||||
$end = (clone $start)->modify('+'.(int)($r['duree_minutes'] ?? 60).' minutes');
|
||||
$when = $start->format('d/m/Y H:i');
|
||||
// 3) Prépare mails
|
||||
$tz = new DateTimeZone('Europe/Paris');
|
||||
$start = new DateTime($r['date_rdv'], $tz);
|
||||
$end = (clone $start)->modify('+' . (int) ($r['duree_minutes'] ?? 60) . ' minutes');
|
||||
$when = $start->format('d/m/Y H:i');
|
||||
|
||||
$pratFull = $r['praticien_prenom'].' '.$r['praticien_nom'];
|
||||
$cliFull = $r['client_prenom'].' '.$r['client_nom'];
|
||||
$pratFull = $r['praticien_prenom'] . ' ' . $r['praticien_nom'];
|
||||
$cliFull = $r['client_prenom'] . ' ' . $r['client_nom'];
|
||||
|
||||
$cta_url_prat = 'https://uslugi.fr/public/praticien/agenda.php';
|
||||
$cta_url_cli = 'https://uslugi.fr/public/client/mes_rendez_vous.php';
|
||||
$cta_label = $translations['consultez_espace_pour_details'] ?? 'Voir les détails';
|
||||
$cta_url_prat = 'https://uslugi.fr/public/praticien/agenda.php';
|
||||
$cta_url_cli = 'https://uslugi.fr/public/client/mes_rendez_vous.php';
|
||||
$cta_label = $translations['consultez_espace_pour_details'] ?? 'Voir les détails';
|
||||
|
||||
$statut_label = $translations['statut_label'] ?? 'Statut';
|
||||
$statut_value = $translations['statut_annule'] ?? 'Annulé';
|
||||
$statut_label = $translations['statut_label'] ?? 'Statut';
|
||||
$statut_value = $translations['statut_annule'] ?? 'Annulé';
|
||||
|
||||
// ---- Mail praticien
|
||||
$subject_p = $translations['email_annulation_sujet'] ?? 'Rendez-vous annulé';
|
||||
$hello_p = ($translations['email_bonjour_dr'] ?? 'Bonjour Dr').' '.$pratFull.',';
|
||||
$intro_p = ($translations['email_rdv_du'] ?? 'Le rendez-vous du').' '.$when.' '.($translations['email_avec'] ?? 'avec').' '.$cliFull.' '.($translations['email_annule_par_client'] ?? 'a été annulé par le client.');
|
||||
// ---- Mail praticien
|
||||
$subject_p = $translations['email_annulation_sujet'] ?? 'Rendez-vous annulé';
|
||||
$hello_p = ($translations['email_bonjour_dr'] ?? 'Bonjour Dr') . ' ' . $pratFull . ',';
|
||||
$intro_p = ($translations['email_rdv_du'] ?? 'Le rendez-vous du') . ' ' . $when . ' ' . ($translations['email_avec'] ?? 'avec') . ' ' . $cliFull . ' ' . ($translations['email_annule_par_client'] ?? 'a été annulé par le client.');
|
||||
|
||||
$data_p = [
|
||||
'hello_line' => $hello_p,
|
||||
'intro' => $intro_p,
|
||||
'who_label' => $translations['client_label'] ?? 'Client',
|
||||
'who_value' => $cliFull,
|
||||
'date_label' => $translations['date_label'] ?? 'Date',
|
||||
'when' => $when,
|
||||
'statut_label'=> $statut_label,
|
||||
'statut_value'=> $statut_value,
|
||||
'cta_url' => $cta_url_prat,
|
||||
'cta_label' => $cta_label,
|
||||
'footer' => $translations['merci_simple'] ?? 'Merci.',
|
||||
];
|
||||
$text_p = email_text_cancel($data_p, $translations);
|
||||
$html_p = email_html_cancel($data_p, $translations);
|
||||
$data_p = [
|
||||
'hello_line' => $hello_p,
|
||||
'intro' => $intro_p,
|
||||
'who_label' => $translations['client_label'] ?? 'Client',
|
||||
'who_value' => $cliFull,
|
||||
'date_label' => $translations['date_label'] ?? 'Date',
|
||||
'when' => $when,
|
||||
'statut_label' => $statut_label,
|
||||
'statut_value' => $statut_value,
|
||||
'cta_url' => $cta_url_prat,
|
||||
'cta_label' => $cta_label,
|
||||
'footer' => $translations['merci_simple'] ?? 'Merci.',
|
||||
];
|
||||
$text_p = email_text_cancel($data_p, $translations);
|
||||
$html_p = email_html_cancel($data_p, $translations);
|
||||
|
||||
$ics_p = null; $ics_name_p = null;
|
||||
if (!empty($r['uuid_ical'])) {
|
||||
$ics_name_p = 'rdv_cancel_'.$r['uuid_ical'].'_praticien.ics';
|
||||
$ics_p = build_ics_cancel(
|
||||
$r['uuid_ical'], $start, $end, 'Europe/Paris',
|
||||
($translations['rdv_avec'] ?? 'Rendez-vous avec').' '.$cliFull,
|
||||
'rdv@uslugi.fr',
|
||||
[
|
||||
['cn'=>$cliFull, 'email'=>$r['client_email']],
|
||||
['cn'=>$pratFull, 'email'=>$r['praticien_email']],
|
||||
]
|
||||
);
|
||||
}
|
||||
[$ok_p, $err_p] = send_email_phpmailer($r['praticien_email'], $subject_p, $text_p, $html_p, $ics_name_p, $ics_p);
|
||||
if (!$ok_p) error_log("Annulation RDV: mail praticien NON envoyé: ".$err_p);
|
||||
$ics_p = null;
|
||||
$ics_name_p = null;
|
||||
if (! empty($r['uuid_ical'])) {
|
||||
$ics_name_p = 'rdv_cancel_' . $r['uuid_ical'] . '_praticien.ics';
|
||||
$ics_p = build_ics_cancel(
|
||||
$r['uuid_ical'], $start, $end, 'Europe/Paris',
|
||||
($translations['rdv_avec'] ?? 'Rendez-vous avec') . ' ' . $cliFull,
|
||||
'rdv@uslugi.fr',
|
||||
[
|
||||
['cn' => $cliFull, 'email' => $r['client_email']],
|
||||
['cn' => $pratFull, 'email' => $r['praticien_email']],
|
||||
]
|
||||
);
|
||||
}
|
||||
[$ok_p, $err_p] = send_email_phpmailer($r['praticien_email'], $subject_p, $text_p, $html_p, $ics_name_p, $ics_p);
|
||||
if (! $ok_p) {
|
||||
error_log("Annulation RDV: mail praticien NON envoyé: " . $err_p);
|
||||
}
|
||||
|
||||
// ---- Mail client
|
||||
$subject_c = $translations['email_annulation_client_sujet'] ?? 'Confirmation d’annulation de votre rendez-vous';
|
||||
$hello_c = sprintf($translations['mail_valid_bonjour_nom'] ?? 'Bonjour %s', $cliFull);
|
||||
$intro_c = ($translations['email_votre_rdv_du'] ?? 'Votre rendez-vous du').' '.$when.' '.($translations['email_avec'] ?? 'avec').' '.(($translations['titre_dr'] ?? 'Dr').' '.$pratFull).' '.($translations['email_a_ete_annule'] ?? 'a été annulé.');
|
||||
// ---- Mail client
|
||||
$subject_c = $translations['email_annulation_client_sujet'] ?? 'Confirmation d’annulation de votre rendez-vous';
|
||||
$hello_c = sprintf($translations['mail_valid_bonjour_nom'] ?? 'Bonjour %s', $cliFull);
|
||||
$intro_c = ($translations['email_votre_rdv_du'] ?? 'Votre rendez-vous du') . ' ' . $when . ' ' . ($translations['email_avec'] ?? 'avec') . ' ' . (($translations['titre_dr'] ?? 'Dr') . ' ' . $pratFull) . ' ' . ($translations['email_a_ete_annule'] ?? 'a été annulé.');
|
||||
|
||||
$who_label_cli = $translations['praticien_label'] ?? ($translations['titre_dr'] ?? 'Dr');
|
||||
$who_value_cli = ($translations['titre_dr'] ?? 'Dr').' '.$pratFull;
|
||||
$who_label_cli = $translations['praticien_label'] ?? ($translations['titre_dr'] ?? 'Dr');
|
||||
$who_value_cli = ($translations['titre_dr'] ?? 'Dr') . ' ' . $pratFull;
|
||||
|
||||
$data_c = [
|
||||
'hello_line' => $hello_c,
|
||||
'intro' => $intro_c,
|
||||
'who_label' => $who_label_cli,
|
||||
'who_value' => $who_value_cli,
|
||||
'date_label' => $translations['date_label'] ?? 'Date',
|
||||
'when' => $when,
|
||||
'statut_label'=> $statut_label,
|
||||
'statut_value'=> $statut_value,
|
||||
'cta_url' => $cta_url_cli,
|
||||
'cta_label' => $cta_label,
|
||||
'footer' => $translations['merci_simple'] ?? 'Merci.',
|
||||
];
|
||||
$text_c = email_text_cancel($data_c, $translations);
|
||||
$html_c = email_html_cancel($data_c, $translations);
|
||||
$data_c = [
|
||||
'hello_line' => $hello_c,
|
||||
'intro' => $intro_c,
|
||||
'who_label' => $who_label_cli,
|
||||
'who_value' => $who_value_cli,
|
||||
'date_label' => $translations['date_label'] ?? 'Date',
|
||||
'when' => $when,
|
||||
'statut_label' => $statut_label,
|
||||
'statut_value' => $statut_value,
|
||||
'cta_url' => $cta_url_cli,
|
||||
'cta_label' => $cta_label,
|
||||
'footer' => $translations['merci_simple'] ?? 'Merci.',
|
||||
];
|
||||
$text_c = email_text_cancel($data_c, $translations);
|
||||
$html_c = email_html_cancel($data_c, $translations);
|
||||
|
||||
$ics_c = null; $ics_name_c = null;
|
||||
if (!empty($r['uuid_ical'])) {
|
||||
$ics_name_c = 'rdv_cancel_'.$r['uuid_ical'].'_client.ics';
|
||||
$ics_c = build_ics_cancel(
|
||||
$r['uuid_ical'], $start, $end, 'Europe/Paris',
|
||||
($translations['rdv_avec'] ?? 'Rendez-vous avec').' '.(($translations['titre_dr'] ?? 'Dr').' '.$pratFull),
|
||||
'rdv@uslugi.fr',
|
||||
[
|
||||
['cn'=>$cliFull, 'email'=>$r['client_email']],
|
||||
['cn'=>$pratFull, 'email'=>$r['praticien_email']],
|
||||
]
|
||||
);
|
||||
}
|
||||
[$ok_c, $err_c] = send_email_phpmailer($r['client_email'], $subject_c, $text_c, $html_c, $ics_name_c, $ics_c);
|
||||
if (!$ok_c) error_log("Annulation RDV: mail client NON envoyé: ".$err_c);
|
||||
$ics_c = null;
|
||||
$ics_name_c = null;
|
||||
if (! empty($r['uuid_ical'])) {
|
||||
$ics_name_c = 'rdv_cancel_' . $r['uuid_ical'] . '_client.ics';
|
||||
$ics_c = build_ics_cancel(
|
||||
$r['uuid_ical'], $start, $end, 'Europe/Paris',
|
||||
($translations['rdv_avec'] ?? 'Rendez-vous avec') . ' ' . (($translations['titre_dr'] ?? 'Dr') . ' ' . $pratFull),
|
||||
'rdv@uslugi.fr',
|
||||
[
|
||||
['cn' => $cliFull, 'email' => $r['client_email']],
|
||||
['cn' => $pratFull, 'email' => $r['praticien_email']],
|
||||
]
|
||||
);
|
||||
}
|
||||
[$ok_c, $err_c] = send_email_phpmailer($r['client_email'], $subject_c, $text_c, $html_c, $ics_name_c, $ics_c);
|
||||
if (! $ok_c) {
|
||||
error_log("Annulation RDV: mail client NON envoyé: " . $err_c);
|
||||
}
|
||||
|
||||
json_out(true, $translations['rdv_annule_ok'] ?? 'Rendez-vous annulé.');
|
||||
json_out(true, $translations['rdv_annule_ok'] ?? 'Rendez-vous annulé.');
|
||||
|
||||
} catch (Throwable $e) {
|
||||
error_log('Annulation RDV error: '.$e->getMessage());
|
||||
json_out(false, $translations['erreur_generique'] ?? 'Erreur serveur.');
|
||||
error_log('Annulation RDV error: ' . $e->getMessage());
|
||||
json_out(false, $translations['erreur_generique'] ?? 'Erreur serveur.');
|
||||
}
|
||||
+56
-39
@@ -1,36 +1,40 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../includes/init_client.php';
|
||||
require_once __DIR__ . '/../includes/init_client.php';
|
||||
|
||||
$success = $erreur = '';
|
||||
$id = $_SESSION['compte_id'];
|
||||
|
||||
$stmt = $pdo->prepare("SELECT email, mot_de_passe FROM compte WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$compte = $stmt->fetch();
|
||||
|
||||
$success = $erreur = '';
|
||||
$id = $_SESSION['compte_id'];
|
||||
if (! $compte) {
|
||||
die($translations['compte_introuvable']);
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("SELECT email FROM compte WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$compte = $stmt->fetch();
|
||||
$email_actuel = $compte['email'];
|
||||
|
||||
if (!$compte) die($translations['compte_introuvable']);
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
require_post_csrf();
|
||||
|
||||
$email_actuel = $compte['email'];
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$nouvel_email = trim($_POST['email'] ?? '');
|
||||
$mdp1 = $_POST['mot_de_passe'] ?? '';
|
||||
$mdp2 = $_POST['confirm_mot_de_passe'] ?? '';
|
||||
$mdpActuel = $_POST['mot_de_passe_actuel'] ?? '';
|
||||
$mdp1 = $_POST['mot_de_passe'] ?? '';
|
||||
$mdp2 = $_POST['confirm_mot_de_passe'] ?? '';
|
||||
|
||||
if (!filter_var($nouvel_email, FILTER_VALIDATE_EMAIL)) {
|
||||
$erreur = $translations['erreur_email_invalide_format'];
|
||||
if (! password_verify($mdpActuel, (string) ($compte['mot_de_passe'] ?? ''))) {
|
||||
$erreur = $translations['erreur_mdp_actuel_incorrect'] ?? 'Mot de passe actuel incorrect.';
|
||||
} elseif (! filter_var($nouvel_email, FILTER_VALIDATE_EMAIL)) {
|
||||
$erreur = $translations['erreur_email_non_valide'];
|
||||
} elseif ($mdp1 && ($mdp1 !== $mdp2 || strlen($mdp1) < 8)) {
|
||||
$erreur = $translations['erreur_mdp_invalide_ou_non_confirme'];
|
||||
$erreur = $translations['erreur_mdp_invalides'];
|
||||
} else {
|
||||
// Vérifie que l'email n’est pas déjà pris (sauf pour soi)
|
||||
$stmt = $pdo->prepare("SELECT id FROM compte WHERE email = ? AND id != ?");
|
||||
$stmt->execute([$nouvel_email, $id]);
|
||||
|
||||
if ($stmt->fetch()) {
|
||||
$erreur = $translations['email_deja_utilise'];
|
||||
$erreur = $translations['err_email_deja_utilise'];
|
||||
} else {
|
||||
// Mise à jour
|
||||
if ($mdp1) {
|
||||
$hash = password_hash($mdp1, PASSWORD_DEFAULT);
|
||||
$stmt = $pdo->prepare("UPDATE compte SET email = ?, mot_de_passe = ? WHERE id = ?");
|
||||
@@ -39,35 +43,48 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$stmt = $pdo->prepare("UPDATE compte SET email = ? WHERE id = ?");
|
||||
$stmt->execute([$nouvel_email, $id]);
|
||||
}
|
||||
$success = $translations['maj_effectuee_succes'];
|
||||
|
||||
$success = $translations['success_compte_mis_a_jour'];
|
||||
$email_actuel = $nouvel_email;
|
||||
|
||||
// garder la valeur en mémoire pour la suite de la page
|
||||
$compte['mot_de_passe'] = $mdp1 ? $hash : $compte['mot_de_passe'];
|
||||
}
|
||||
}
|
||||
}
|
||||
require_once '../includes/header.php';
|
||||
}
|
||||
require_once '../includes/header.php';
|
||||
?>
|
||||
|
||||
<div class="main1">
|
||||
<div class="container py-5">
|
||||
|
||||
|
||||
|
||||
<?php if ($erreur): ?>
|
||||
<div class="alert alert-danger"><?= $erreur ?></div>
|
||||
<div class="alert alert-danger"><?php echo $erreur ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($success): ?>
|
||||
<div class="alert alert-success"><?= $success ?></div>
|
||||
<div class="alert alert-success"><?php echo $success ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="POST" class="shadow p-4 rounded bg-light">
|
||||
<h2 class="text-center mb-4"> <?= $translations['mes_infos_connexion'] ?></h2>
|
||||
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(ensure_csrf_token(), ENT_QUOTES, 'UTF-8') ?>">
|
||||
<h2 class="text-center mb-4"> <?php echo $translations['mes_infos_connexion'] ?></h2>
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= $translations['label_email'] ?></label>
|
||||
<input type="email" name="email" value="<?= htmlspecialchars($email_actuel) ?>" required class="form-control">
|
||||
<label class="form-label"><?php echo $translations['label_email'] ?></label>
|
||||
<input type="email" name="email" value="<?php echo htmlspecialchars($email_actuel) ?>" required class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?php echo htmlspecialchars($translations['label_mdp_actuel'] ?? 'Mot de passe actuel') ?></label>
|
||||
<div class="field-wrapper">
|
||||
<input type="password" name="mot_de_passe_actuel" id="mot_de_passe_actuel" class="form-control"
|
||||
placeholder="<?php echo htmlspecialchars($translations['placeholder_mdp_actuel'] ?? 'Entrez votre mot de passe actuel') ?>" required>
|
||||
<span class="toggle-password" onclick="toggle('mot_de_passe_actuel')">👁️</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= $translations['label_nouveau_mdp_facultatif'] ?></label>
|
||||
<label class="form-label"><?php echo $translations['label_nouveau_mdp_facultatif'] ?></label>
|
||||
<div class="field-wrapper">
|
||||
<input type="password" name="mot_de_passe" id="mot_de_passe" placeholder="<?= $translations['nouveau_mdp_placeholder'] ?>" class="form-control">
|
||||
<input type="password" name="mot_de_passe" id="mot_de_passe" placeholder="<?php echo $translations['nouveau_mdp_placeholder'] ?>" class="form-control">
|
||||
<span class="toggle-password" onclick="toggle('mot_de_passe')">👁️</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -77,18 +94,18 @@ require_once '../includes/header.php';
|
||||
<div class="strength-fill"></div>
|
||||
<div class="strength-fill"></div>
|
||||
</div>
|
||||
<div id="niveau-securite" class="mb-3 text-muted"><?= $translations['securite_indetermine'] ?></div>
|
||||
<div id="niveau-securite" class="mb-3 text-muted"><?php echo $translations['securite_indetermine'] ?></div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="form-label"><?= $translations['label_confirmer_mdp'] ?></label>
|
||||
<label class="form-label"><?php echo $translations['label_confirmer_mdp'] ?></label>
|
||||
<div class="field-wrapper">
|
||||
<input type="password" name="confirm_mot_de_passe" id="confirm_mot_de_passe" placeholder="<?= $translations['confirm_mdp_placeholder'] ?>" class="form-control">
|
||||
<input type="password" name="confirm_mot_de_passe" id="confirm_mot_de_passe" placeholder="<?php echo $translations['confirm_mdp_placeholder'] ?>" class="form-control">
|
||||
<span class="toggle-password" onclick="toggle('confirm_mot_de_passe')">👁️</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<button type="submit" class="btn btn-primary"> <?= $translations['btn_mettre_a_jour'] ?></button>
|
||||
<button type="submit" class="btn btn-primary"> <?php echo $translations['btn_mettre_a_jour'] ?></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -104,7 +121,7 @@ document.getElementById('mot_de_passe').addEventListener('input', function () {
|
||||
const niveauText = document.getElementById("niveau-securite");
|
||||
|
||||
segments.forEach(s => s.style.backgroundColor = '#e0e0e0');
|
||||
niveauText.textContent = <?= json_encode($translations['securite_indetermine']) ?>;
|
||||
niveauText.textContent = <?php echo json_encode($translations['securite_indetermine']) ?>;
|
||||
niveauText.className = 'text-muted';
|
||||
|
||||
const longOk = val.length >= 8;
|
||||
@@ -120,16 +137,16 @@ document.getElementById('mot_de_passe').addEventListener('input', function () {
|
||||
|
||||
if (force === 1) {
|
||||
segments[0].style.backgroundColor = 'red';
|
||||
niveauText.textContent = <?= json_encode($translations['securite_faible']) ?>;
|
||||
niveauText.textContent = <?php echo json_encode($translations['securite_faible']) ?>;
|
||||
niveauText.className = 'text-danger';
|
||||
} else if (force === 2) {
|
||||
segments[0].style.backgroundColor = 'orange';
|
||||
segments[1].style.backgroundColor = 'orange';
|
||||
niveauText.textContent = <?= json_encode($translations['securite_moyen']) ?>;
|
||||
niveauText.textContent = <?php echo json_encode($translations['securite_moyen']) ?>;
|
||||
niveauText.className = 'text-warning';
|
||||
} else if (force >= 3) {
|
||||
segments.forEach(s => s.style.backgroundColor = 'green');
|
||||
niveauText.textContent = <?= json_encode($translations['securite_fort']) ?>;
|
||||
niveauText.textContent = <?php echo json_encode($translations['securite_fort']) ?>;
|
||||
niveauText.className = 'text-success';
|
||||
}
|
||||
});
|
||||
@@ -137,7 +154,7 @@ document.getElementById('mot_de_passe').addEventListener('input', function () {
|
||||
</div>
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p><?= $translations['copyr'] ?></p>
|
||||
<p><?php echo $translations['copyr'] ?></p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
|
||||
+159
-115
@@ -1,60 +1,102 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../includes/init_client.php';
|
||||
require_once __DIR__ . '/../includes/init_client.php';
|
||||
|
||||
$praticien_id = (int) ($_GET['id'] ?? 0);
|
||||
if ($praticien_id <= 0) {echo "<div class='container py-5'>ID invalide.</div>";exit;}
|
||||
|
||||
$currentLang = $lang ?? 'ru';
|
||||
if (! in_array($currentLang, ['ru', 'fr', 'en'], true)) {
|
||||
$currentLang = 'ru';
|
||||
}
|
||||
|
||||
$praticien_id = (int)($_GET['id'] ?? 0);
|
||||
if ($praticien_id <= 0) { echo "<div class='container py-5'>ID invalide.</div>"; exit; }
|
||||
function h($s)
|
||||
{return htmlspecialchars((string) $s, ENT_QUOTES, 'UTF-8');}
|
||||
function percent_from_5($note)
|
||||
{$n = max(0, min(5, (float) $note));return (int) round($n / 5 * 100);}
|
||||
function pseudo_from($prenom, $nom)
|
||||
{
|
||||
$p = trim((string) $prenom);
|
||||
$n = trim((string) $nom);
|
||||
$p = $p !== '' ? mb_convert_case($p, MB_CASE_TITLE, 'UTF-8') : 'Client';
|
||||
$i = $n !== '' ? mb_strtoupper(mb_substr($nom, 0, 1, 'UTF-8'), 'UTF-8') : '';
|
||||
return $i ? ($p . '.' . $i) : $p;
|
||||
}
|
||||
function video_thumb_info(?string $url): ?array
|
||||
{
|
||||
if (! $url || ! trim($url)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
|
||||
function percent_from_5($note){ $n = max(0,min(5,(float)$note)); return (int)round($n/5*100); }
|
||||
function pseudo_from($prenom,$nom){
|
||||
$p = trim((string)$prenom); $n = trim((string)$nom);
|
||||
$p = $p!=='' ? mb_convert_case($p, MB_CASE_TITLE, 'UTF-8') : 'Client';
|
||||
$i = $n!=='' ? mb_strtoupper(mb_substr($nom,0,1,'UTF-8'),'UTF-8') : '';
|
||||
return $i ? ($p.'.'.$i) : $p;
|
||||
}
|
||||
function video_thumb_info(?string $url): ?array {
|
||||
if (!$url || !trim($url)) return null; $u = trim($url);
|
||||
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"];
|
||||
}
|
||||
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"];
|
||||
}
|
||||
$path=parse_url($u,PHP_URL_PATH)??''; if(preg_match('~\.(mp4|webm|ogg)$~i',$path)){ return ['provider'=>'file','id'=>null,'thumb'=>null]; }
|
||||
if (stripos($u,'vimeo.com')!==false){ return ['provider'=>'vimeo','id'=>null,'thumb'=>null]; }
|
||||
return ['provider'=>'other','id'=>null,'thumb'=>null];
|
||||
}
|
||||
$u = trim($url);
|
||||
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"];
|
||||
}
|
||||
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"];
|
||||
}
|
||||
$path = parse_url($u, PHP_URL_PATH) ?? '';if (preg_match('~\.(mp4|webm|ogg)$~i', $path)) {return ['provider' => 'file', 'id' => null, 'thumb' => null];}
|
||||
if (stripos($u, 'vimeo.com') !== false) {return ['provider' => 'vimeo', 'id' => null, 'thumb' => null];}
|
||||
return ['provider' => 'other', 'id' => null, 'thumb' => null];
|
||||
}
|
||||
|
||||
// 1) Praticien
|
||||
$st = $pdo->prepare("SELECT id, titre, nom, prenom, telephone, adresse, ville, code_postal, specialite, presentation, photo, video, disponibilite
|
||||
FROM praticien WHERE id = ?");
|
||||
$st->execute([$praticien_id]);
|
||||
$p = $st->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$p) { echo "<div class='container py-5'>".$translations['praticien_introuvable']."</div>"; exit; }
|
||||
// 1) Praticien
|
||||
$st = $pdo->prepare("
|
||||
SELECT
|
||||
p.id,
|
||||
p.titre,
|
||||
p.nom,
|
||||
p.prenom,
|
||||
p.telephone,
|
||||
p.adresse,
|
||||
p.ville,
|
||||
p.code_postal,
|
||||
p.specialite,
|
||||
p.presentation,
|
||||
p.photo,
|
||||
p.video,
|
||||
p.disponibilite,
|
||||
COALESCE(pt.nom_affiche, p.nom) AS nom_affiche,
|
||||
COALESCE(pt.prenom_affiche, p.prenom) AS prenom_affiche,
|
||||
COALESCE(pt.ville_affichee, p.ville) AS ville_affichee,
|
||||
COALESCE(pt.titre, p.titre) AS titre_affiche,
|
||||
COALESCE(pt.specialite, p.specialite) AS specialite_affichee,
|
||||
COALESCE(pt.presentation, p.presentation) AS presentation_affichee,
|
||||
COALESCE(pt.disponibilite, p.disponibilite) AS disponibilite_affichee
|
||||
FROM praticien p
|
||||
LEFT JOIN praticien_traduction pt
|
||||
ON pt.praticien_id = p.id
|
||||
AND pt.langue = :lang
|
||||
WHERE p.id = :id
|
||||
LIMIT 1
|
||||
");
|
||||
$st->execute([
|
||||
':lang' => $currentLang,
|
||||
':id' => $praticien_id,
|
||||
]);
|
||||
$p = $st->fetch(PDO::FETCH_ASSOC);
|
||||
if (! $p) {echo "<div class='container py-5'>" . $translations['praticien_introuvable'] . "</div>";exit;}
|
||||
|
||||
$name = $name = trim($p['prenom'] . ' ' . $p['nom']);
|
||||
$photo = !empty($p['photo']) ? "/public/uploads/".ltrim($p['photo'],'/') : "/public/uploads/img/user_placeholder.png";
|
||||
$name = trim(($p['prenom_affiche'] ?? $p['prenom'] ?? '') . ' ' . ($p['nom_affiche'] ?? $p['nom'] ?? ''));
|
||||
$photo = ! empty($p['photo']) ? "/public/uploads/" . ltrim($p['photo'], '/') : "/public/uploads/img/user_placeholder.png";
|
||||
|
||||
// 2) Vidéos additionnelles
|
||||
$vs = $pdo->prepare("SELECT id, url, title, ordre FROM praticien_video WHERE praticien_id=? ORDER BY ordre ASC, id DESC");
|
||||
$vs->execute([$praticien_id]);
|
||||
$videos = $vs->fetchAll(PDO::FETCH_ASSOC);
|
||||
// 2) Vidéos additionnelles
|
||||
$vs = $pdo->prepare("SELECT id, url, title, ordre FROM praticien_video WHERE praticien_id=? ORDER BY ordre ASC, id DESC");
|
||||
$vs->execute([$praticien_id]);
|
||||
$videos = $vs->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// 3) Avis: moyenne + total + dernier
|
||||
$avg = $pdo->prepare("SELECT ROUND(AVG(note),1) AS m, COUNT(*) AS c FROM avis_praticien WHERE praticien_id=? AND statut='publie'");
|
||||
$avg->execute([$praticien_id]);
|
||||
$stats = $avg->fetch(PDO::FETCH_ASSOC) ?: ['m'=>null,'c'=>0];
|
||||
// 3) Avis: moyenne + total + dernier
|
||||
$avg = $pdo->prepare("SELECT ROUND(AVG(note),1) AS m, COUNT(*) AS c FROM avis_praticien WHERE praticien_id=? AND statut='publie'");
|
||||
$avg->execute([$praticien_id]);
|
||||
$stats = $avg->fetch(PDO::FETCH_ASSOC) ?: ['m' => null, 'c' => 0];
|
||||
|
||||
$last = $pdo->prepare("SELECT a.note,a.titre,a.commentaire,a.visit_date,a.created_at,c.prenom,c.nom
|
||||
$last = $pdo->prepare("SELECT a.note,a.titre,a.commentaire,a.visit_date,a.created_at,c.prenom,c.nom
|
||||
FROM avis_praticien a
|
||||
LEFT JOIN client c ON c.id=a.client_id
|
||||
WHERE a.praticien_id=? AND a.statut='publie'
|
||||
ORDER BY a.created_at DESC LIMIT 1");
|
||||
$last->execute([$praticien_id]);
|
||||
$lrow = $last->fetch(PDO::FETCH_ASSOC);
|
||||
require_once __DIR__.'/../includes/header.php'; // inclut lang + styles
|
||||
$last->execute([$praticien_id]);
|
||||
$lrow = $last->fetch(PDO::FETCH_ASSOC);
|
||||
require_once __DIR__ . '/../includes/header.php'; // inclut lang + styles
|
||||
?>
|
||||
<div class="main1">
|
||||
<div class="container py-4">
|
||||
@@ -63,75 +105,75 @@ require_once __DIR__.'/../includes/header.php'; // inclut lang + styles
|
||||
<!-- Cadre 1 : Infos -->
|
||||
<section class="panel">
|
||||
<div class="info-scroll">
|
||||
<h2 class="pro-name"><?= h($name) ?></h2>
|
||||
<?php if($p['specialite']): ?><div><strong><?= h($translations['dp_specialite']) ?>:</strong> <?= h($p['specialite']) ?></div><?php endif; ?>
|
||||
<!--<?php if($p['telephone']): ?><div><strong><?= h($translations['dp_telephone']) ?>:</strong> <?= h($p['telephone']) ?></div><?php endif; ?>-->
|
||||
<?php if($p['titre']): ?><div><strong><?= h($translations['tarif_consultation']) ?>:</strong> <?= h($p['titre']) ?></div><?php endif; ?>
|
||||
<?php if($p['disponibilite']): ?><div><strong><?= h($translations['moyens_payement']) ?>:</strong> <?= h($p['disponibilite']) ?></div><?php endif; ?>
|
||||
<?php if($p['adresse']): ?><div><strong><?= h($translations['dp_adresse']) ?>:</strong> <?= nl2br(h($p['adresse'])) ?></div><?php endif; ?>
|
||||
<?php if($p['ville'] || $p['code_postal']): ?><div><strong><?= h($translations['dp_ville_cp']) ?>:</strong> <?= h(trim(($p['ville']??'').' '.($p['code_postal']??''))) ?></div><?php endif; ?>
|
||||
<?php if($p['presentation']): ?><div class="mt-3"><strong><?= h($translations['dp_description']) ?>:</strong><br><?= nl2br(h($p['presentation'])) ?></div><?php endif; ?>
|
||||
<h2 class="pro-name"><?php echo h($name) ?></h2>
|
||||
<?php if ($p['specialite_affichee']): ?><div><strong><?php echo h($translations['dp_specialite']) ?>:</strong> <?php echo h($p['specialite_affichee']) ?></div><?php endif; ?>
|
||||
<!--<?php if ($p['telephone']): ?><div><strong><?php echo h($translations['dp_telephone']) ?>:</strong> <?php echo h($p['telephone']) ?></div><?php endif; ?>-->
|
||||
<?php if ($p['titre_affiche']): ?><div><strong><?php echo h($translations['tarif_consultation']) ?>:</strong> <?php echo h($p['titre_affiche']) ?></div><?php endif; ?>
|
||||
<?php if ($p['disponibilite_affichee']): ?><div><strong><?php echo h($translations['moyens_payement']) ?>:</strong> <?php echo h($p['disponibilite_affichee']) ?></div><?php endif; ?>
|
||||
<?php if ($p['adresse']): ?><div><strong><?php echo h($translations['dp_adresse']) ?>:</strong> <?php echo nl2br(h($p['adresse'])) ?></div><?php endif; ?>
|
||||
<?php if ($p['ville_affichee'] || $p['code_postal']): ?><div><strong><?php echo h($translations['dp_ville_cp']) ?>:</strong> <?php echo h(trim(($p['ville_affichee'] ?? '') . ' ' . ($p['code_postal'] ?? ''))) ?></div><?php endif; ?>
|
||||
<?php if ($p['presentation_affichee']): ?><div class="mt-3"><strong><?php echo h($translations['dp_description']) ?>:</strong><br><?php echo nl2br(h($p['presentation_affichee'])) ?></div><?php endif; ?>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Centre : grande photo -->
|
||||
<section class="big-photo">
|
||||
<img class="img-fluid w-100" src="<?= h($photo) ?>" alt="Photo">
|
||||
<img class="img-fluid w-100" src="<?php echo h($photo) ?>" alt="Photo">
|
||||
</section>
|
||||
|
||||
<!-- Cadre 2 : Vidéos -->
|
||||
<?php if($p['video'] != ''){ ?>
|
||||
<?php if ($p['video'] != '') {?>
|
||||
<aside class="panel">
|
||||
<h2 class="pro-name"><?= h($translations['dp_videos']) ?></h2>
|
||||
<h2 class="pro-name"><?php echo h($translations['dp_videos']) ?></h2>
|
||||
<div class="vlist">
|
||||
<?php
|
||||
$alt = h($translations['video_alt'] ?? 'Vidéo');
|
||||
$printed = false;
|
||||
$alt = h($translations['video_alt'] ?? 'Vidéo');
|
||||
$printed = false;
|
||||
|
||||
// Petite fonction d'affichage d’une vignette
|
||||
$printThumb = function($provider, $id, $thumb, $rawUrl) use ($alt){
|
||||
if (in_array($provider, ['youtube','dailymotion']) && $id){
|
||||
echo '<a href="#" class="vitem d-block pro-video"
|
||||
// Petite fonction d'affichage d’une vignette
|
||||
$printThumb = function ($provider, $id, $thumb, $rawUrl) use ($alt) {
|
||||
if (in_array($provider, ['youtube', 'dailymotion']) && $id) {
|
||||
echo '<a href="#" class="vitem d-block pro-video"
|
||||
data-bs-toggle="modal" data-bs-target="#videoModal"
|
||||
data-provider="'.h($provider).'" data-id="'.h($id).'">';
|
||||
echo $thumb
|
||||
? '<img src="'.h($thumb).'" alt="'.$alt.'" loading="lazy">'
|
||||
: '<img src="/public/uploads/img/video_placeholder.jpg" alt="'.$alt.'" loading="lazy">';
|
||||
echo '</a>';
|
||||
} else {
|
||||
$safe = h($rawUrl);
|
||||
echo '<a class="vitem d-block" href="'.$safe.'" target="_blank" rel="noopener">';
|
||||
echo $thumb
|
||||
? '<img src="'.h($thumb).'" alt="'.$alt.'" loading="lazy">'
|
||||
: '<img src="/public/uploads/img/video_placeholder.jpg" alt="'.$alt.'" loading="lazy">';
|
||||
echo '</a>';
|
||||
}
|
||||
};
|
||||
data-provider="' . h($provider) . '" data-id="' . h($id) . '">';
|
||||
echo $thumb
|
||||
? '<img src="' . h($thumb) . '" alt="' . $alt . '" loading="lazy">'
|
||||
: '<img src="/public/uploads/img/video_placeholder.jpg" alt="' . $alt . '" loading="lazy">';
|
||||
echo '</a>';
|
||||
} else {
|
||||
$safe = h($rawUrl);
|
||||
echo '<a class="vitem d-block" href="' . $safe . '" target="_blank" rel="noopener">';
|
||||
echo $thumb
|
||||
? '<img src="' . h($thumb) . '" alt="' . $alt . '" loading="lazy">'
|
||||
: '<img src="/public/uploads/img/video_placeholder.jpg" alt="' . $alt . '" loading="lazy">';
|
||||
echo '</a>';
|
||||
}
|
||||
};
|
||||
|
||||
if (!empty($p['video'])) {
|
||||
$vi = video_thumb_info($p['video']);
|
||||
$thumb = $vi['thumb'] ?? null;
|
||||
$prov = $vi['provider'] ?? null;
|
||||
$vid = $vi['id'] ?? null;
|
||||
$printThumb($prov, $vid, $thumb, $p['video']);
|
||||
$printed = true;
|
||||
}
|
||||
foreach ($videos as $v) {
|
||||
$vi = video_thumb_info($v['url']);
|
||||
$thumb = $vi['thumb'] ?? null;
|
||||
$prov = $vi['provider'] ?? null;
|
||||
$vid = $vi['id'] ?? null;
|
||||
$printThumb($prov, $vid, $thumb, $v['url']);
|
||||
$printed = true;
|
||||
}
|
||||
if (!$printed) {
|
||||
echo '<div class="vitem"><img src="/public/uploads/img/video_placeholder.jpg" alt="'.$alt.'"></div>';
|
||||
//echo '<div class="text-muted small mt-1">'.h($translations['dp_aucune_video']).'</div>';
|
||||
}
|
||||
?>
|
||||
if (! empty($p['video'])) {
|
||||
$vi = video_thumb_info($p['video']);
|
||||
$thumb = $vi['thumb'] ?? null;
|
||||
$prov = $vi['provider'] ?? null;
|
||||
$vid = $vi['id'] ?? null;
|
||||
$printThumb($prov, $vid, $thumb, $p['video']);
|
||||
$printed = true;
|
||||
}
|
||||
foreach ($videos as $v) {
|
||||
$vi = video_thumb_info($v['url']);
|
||||
$thumb = $vi['thumb'] ?? null;
|
||||
$prov = $vi['provider'] ?? null;
|
||||
$vid = $vi['id'] ?? null;
|
||||
$printThumb($prov, $vid, $thumb, $v['url']);
|
||||
$printed = true;
|
||||
}
|
||||
if (! $printed) {
|
||||
echo '<div class="vitem"><img src="/public/uploads/img/video_placeholder.jpg" alt="' . $alt . '"></div>';
|
||||
//echo '<div class="text-muted small mt-1">'.h($translations['dp_aucune_video']).'</div>';
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
</aside>
|
||||
<?php } ?>
|
||||
<?php }?>
|
||||
</div>
|
||||
|
||||
<!-- Ligne du bas : Note / Dernier avis / Bouton RDV -->
|
||||
@@ -139,14 +181,16 @@ require_once __DIR__.'/../includes/header.php'; // inclut lang + styles
|
||||
<!-- Cadre 3 : Note -->
|
||||
<div class="card shadow-sm p-3">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<?php $m=(float)($stats['m'] ?? 0); $c=(int)($stats['c'] ?? 0); $w=percent_from_5($m); ?>
|
||||
<?php $m = (float) ($stats['m'] ?? 0);
|
||||
$c = (int) ($stats['c'] ?? 0);
|
||||
$w = percent_from_5($m); ?>
|
||||
<div class="rating" aria-label="Note moyenne">
|
||||
<div class="stars-base">★★★★★</div>
|
||||
<div class="stars-fill" style="width:<?= $w ?>%">★★★★★</div>
|
||||
<div class="stars-fill" style="width:<?php echo $w ?>%">★★★★★</div>
|
||||
</div>
|
||||
<div>
|
||||
<div><strong><?= h($translations['avis_note_moy']) ?>:</strong> <?= $m ? number_format($m,1,',',' ') : '—' ?>/5</div>
|
||||
<div class="text-muted">(<?= $c.' '.h($translations['avis_total']) ?>)</div>
|
||||
<div><strong><?php echo h($translations['avis_note_moy']) ?>:</strong> <?php echo $m ? number_format($m, 1, ',', ' ') : '—' ?>/5</div>
|
||||
<div class="text-muted">(<?php echo $c . ' ' . h($translations['avis_total']) ?>)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -154,31 +198,31 @@ require_once __DIR__.'/../includes/header.php'; // inclut lang + styles
|
||||
<!-- Cadre 4 : Dernier avis -->
|
||||
<div class="card shadow-sm p-3 avis-card">
|
||||
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||
<strong><?= h($translations['avis_dernier']) ?></strong>
|
||||
<a class="btn btn-link p-0" href="/public/client/avis.php?praticien_id=<?= (int)$praticien_id ?>">
|
||||
<?= h($translations['avis_lire_tous']) ?>
|
||||
<strong><?php echo h($translations['avis_dernier']) ?></strong>
|
||||
<a class="btn btn-link p-0" href="/public/client/avis.php?praticien_id=<?php echo (int) $praticien_id ?>">
|
||||
<?php echo h($translations['avis_lire_tous']) ?>
|
||||
</a>
|
||||
</div>
|
||||
<?php if ($lrow): ?>
|
||||
<?php $lw=percent_from_5((float)$lrow['note']); ?>
|
||||
<?php $lw = percent_from_5((float) $lrow['note']); ?>
|
||||
<div class="rating mb-1">
|
||||
<div class="stars-base">★★★★★</div>
|
||||
<div class="stars-fill" style="width:<?= $lw ?>%">★★★★★</div>
|
||||
<div class="stars-fill" style="width:<?php echo $lw ?>%">★★★★★</div>
|
||||
</div>
|
||||
<div class="meta">
|
||||
<?= h(pseudo_from($lrow['prenom'] ?? '', $lrow['nom'] ?? '')) ?>
|
||||
<?php if(!empty($lrow['visit_date'])): ?> • <?= date('d/m/Y', strtotime($lrow['visit_date'])) ?><?php endif; ?>
|
||||
<?php echo h(pseudo_from($lrow['prenom'] ?? '', $lrow['nom'] ?? '')) ?>
|
||||
<?php if (! empty($lrow['visit_date'])): ?> • <?php echo date('d/m/Y', strtotime($lrow['visit_date'])) ?><?php endif; ?>
|
||||
</div>
|
||||
<div><?= nl2br(h(mb_strimwidth((string)$lrow['commentaire'],0,320,'…','UTF-8'))) ?></div>
|
||||
<div><?php echo nl2br(h(mb_strimwidth((string) $lrow['commentaire'], 0, 320, '…', 'UTF-8'))) ?></div>
|
||||
<?php else: ?>
|
||||
<div class="text-muted"><?= h($translations['avis_aucun']) ?></div>
|
||||
<div class="text-muted"><?php echo h($translations['avis_aucun']) ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- Bouton RDV -->
|
||||
<div class="text-end">
|
||||
<a class="btn btn-danger btn-lg" href="/public/client/reserver_rdv.php?praticien_id=<?= (int)$praticien_id ?>">
|
||||
<?= h($translations['dp_btn_rdv']) ?>
|
||||
<a class="btn btn-danger btn-lg" href="/public/client/reserver_rdv.php?praticien_id=<?php echo (int) $praticien_id ?>">
|
||||
<?php echo h($translations['dp_btn_rdv']) ?>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -191,8 +235,8 @@ require_once __DIR__.'/../includes/header.php'; // inclut lang + styles
|
||||
<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"><?= h($translations['video_titre'] ?? 'Vidéo') ?></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="<?= h($translations['fermer'] ?? 'Fermer') ?>"></button>
|
||||
<h5 class="modal-title" id="videoModalLabel"><?php echo h($translations['video_titre'] ?? 'Vidéo') ?></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="<?php echo h($translations['fermer'] ?? 'Fermer') ?>"></button>
|
||||
</div>
|
||||
<div class="modal-body pt-0">
|
||||
<div class="ratio ratio-16x9 rounded-3 overflow-hidden bg-black">
|
||||
@@ -204,7 +248,7 @@ require_once __DIR__.'/../includes/header.php'; // inclut lang + styles
|
||||
</div>
|
||||
<div class="modal-footer border-0">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
<?= h($translations['fermer'] ?? 'Fermer') ?>
|
||||
<?php echo h($translations['fermer'] ?? 'Fermer') ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -241,7 +285,7 @@ require_once __DIR__.'/../includes/header.php'; // inclut lang + styles
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p><?= $translations['copyr'] ?></p>
|
||||
<p><?php echo $translations['copyr'] ?></p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
|
||||
+303
-218
@@ -1,218 +1,271 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../includes/init_client.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);
|
||||
// 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 !== '');
|
||||
$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 !== '');
|
||||
$currentLang = $lang ?? 'ru';
|
||||
if (! in_array($currentLang, ['ru', 'fr', 'en'], true)) {
|
||||
$currentLang = 'ru';
|
||||
}
|
||||
|
||||
/* =========================
|
||||
/* =========================
|
||||
Helpers
|
||||
========================= */
|
||||
function video_thumb_info(?string $url): ?array {
|
||||
if (!$url || !trim($url)) return null;
|
||||
$u = trim($url);
|
||||
function video_thumb_info(?string $url): ?array
|
||||
{
|
||||
if (! $url || ! trim($url)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 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];
|
||||
}
|
||||
$u = trim($url);
|
||||
|
||||
function fullName(array $p): string {
|
||||
return trim(($p['prenom'] ?? '').' '.($p['nom'] ?? ''));
|
||||
}
|
||||
// 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];
|
||||
}
|
||||
|
||||
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)).'…';
|
||||
}
|
||||
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_affiche'] ?? $p['prenom'] ?? '') . ' ' . ($p['nom_affiche'] ?? $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 = [];
|
||||
$whereSql = [];
|
||||
$params = [];
|
||||
|
||||
/*==========================BLOCK WHERE AVEC ABONNEMENT VALIDE ==========================*/
|
||||
$whereSql[] = "(date_expiration IS NOT NULL AND date_expiration >= DATE_SUB(NOW(), INTERVAL 14 DAY))";
|
||||
/*=======================================================================================*/
|
||||
/*==========================BLOCK WHERE AVEC ABONNEMENT VALIDE ==========================*/
|
||||
$whereSql[] = "(p.date_expiration IS NOT NULL AND p.date_expiration >= DATE_SUB(NOW(), INTERVAL 14 DAY))";
|
||||
/*=======================================================================================*/
|
||||
|
||||
if ($q !== '') {
|
||||
$words = preg_split('/\s+/', $q, -1, PREG_SPLIT_NO_EMPTY);
|
||||
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 = [];
|
||||
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
|
||||
)";
|
||||
$and[] = "(
|
||||
LOWER(CONCAT(COALESCE(pt.prenom_affiche, p.prenom, ''),' ',COALESCE(pt.nom_affiche, p.nom, ''))) LIKE $k1
|
||||
OR LOWER(COALESCE(pt.specialite, p.specialite, '')) LIKE $k2
|
||||
)";
|
||||
|
||||
$params[$k1] = '%'.$word.'%';
|
||||
$params[$k2] = '%'.$word.'%';
|
||||
}
|
||||
$params[$k1] = '%' . $word . '%';
|
||||
$params[$k2] = '%' . $word . '%';
|
||||
}
|
||||
|
||||
$whereSql[] = '(' . implode(' AND ', $and) . ')';
|
||||
}
|
||||
$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));
|
||||
|
||||
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 = [];
|
||||
|
||||
$ors = [];
|
||||
// Ville (LOWER(...)) : on cherche par préfixe
|
||||
foreach ($villeCands as $i => $cand) {
|
||||
$key = ":wv$i";
|
||||
$ors[] = "LOWER(COALESCE(pt.ville_affichee, p.ville, '')) LIKE $key";
|
||||
$params[$key] = $cand . '%'; // ← passe en '%'.$cand.'%' si tu veux "contient"
|
||||
}
|
||||
|
||||
// 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(p.code_postal,'') LIKE :wcp";
|
||||
$params[':wcp'] = '%' . $w . '%';
|
||||
|
||||
// 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)) : '';
|
||||
|
||||
$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
|
||||
/* Total pour pagination (utile seulement si !isSearch, mais on calcule une fois) */
|
||||
$sqlCount = "
|
||||
SELECT COUNT(*)
|
||||
FROM praticien p
|
||||
LEFT JOIN praticien_traduction pt
|
||||
ON pt.praticien_id = p.id
|
||||
AND pt.langue = :lang
|
||||
$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);
|
||||
|
||||
/* =========================
|
||||
$stmt = $pdo->prepare($sqlCount);
|
||||
$paramsCount = $params;
|
||||
$paramsCount[':lang'] = $currentLang;
|
||||
$stmt->execute($paramsCount);
|
||||
$total = (int) $stmt->fetchColumn();
|
||||
$totalPages = max(1, (int) ceil($total / $perPage));
|
||||
|
||||
/* Liste (paginée si pas de recherche, sinon tout) */
|
||||
$sqlList = "
|
||||
SELECT
|
||||
p.id,
|
||||
p.titre,
|
||||
p.nom,
|
||||
p.prenom,
|
||||
p.photo,
|
||||
p.ville,
|
||||
p.code_postal,
|
||||
p.video,
|
||||
COALESCE(pt.nom_affiche, p.nom) AS nom_affiche,
|
||||
COALESCE(pt.prenom_affiche, p.prenom) AS prenom_affiche,
|
||||
COALESCE(pt.specialite, p.specialite) AS specialite_affichee,
|
||||
COALESCE(pt.ville_affichee, p.ville) AS ville_affichee,
|
||||
COALESCE(pt.presentation, p.presentation) AS presentation_affichee
|
||||
FROM praticien p
|
||||
LEFT JOIN praticien_traduction pt
|
||||
ON pt.praticien_id = p.id
|
||||
AND pt.langue = :lang
|
||||
$whereClause
|
||||
ORDER BY COALESCE(pt.specialite, p.specialite) ASC, COALESCE(pt.nom_affiche, p.nom) ASC
|
||||
";
|
||||
if (! $isSearch) {
|
||||
$sqlList .= " LIMIT :limit OFFSET :offset";
|
||||
}
|
||||
$stmt = $pdo->prepare($sqlList);
|
||||
$params[':lang'] = $currentLang;
|
||||
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 {
|
||||
?>
|
||||
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>
|
||||
<h1 class="results-title"><?php echo 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');
|
||||
?>
|
||||
$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') : '';
|
||||
?>
|
||||
$name = fullName($p);
|
||||
$spec = $p['specialite_affichee'] ?? $p['specialite'] ?? '';
|
||||
$ville = $p['ville_affichee'] ?? $p['ville'] ?? '';
|
||||
$cp = $p['code_postal'] ?? '';
|
||||
$present = excerpt_words((string) ($p['presentation_affichee'] ?? $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') ?>">
|
||||
data-name="<?php echo htmlspecialchars(mb_strtolower($name), ENT_QUOTES, 'UTF-8') ?>"
|
||||
data-spec="<?php echo htmlspecialchars(mb_strtolower($spec), ENT_QUOTES, 'UTF-8') ?>"
|
||||
data-ville="<?php echo htmlspecialchars(mb_strtolower($ville), ENT_QUOTES, 'UTF-8') ?>"
|
||||
data-cp="<?php echo 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') ?>"
|
||||
<a href="/public/client/detail_praticien.php?id=<?php echo (int) $p['id'] ?>"><img
|
||||
src="<?php echo ! empty($p['photo']) ? '/public/uploads/' . htmlspecialchars($p['photo'], ENT_QUOTES, 'UTF-8') : '/public/uploads/img/user_placeholder.png' ?>"
|
||||
alt="<?php echo 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; ?>
|
||||
<h2 class="pro-name"><a href="/public/client/detail_praticien.php?id=<?php echo (int) $p['id'] ?>"><?php echo htmlspecialchars($name, ENT_QUOTES, 'UTF-8') ?></a></h2>
|
||||
<?php if ($spec): ?><div class="pro-spec"><?php echo htmlspecialchars($spec, ENT_QUOTES, 'UTF-8') ?></div><?php endif; ?>
|
||||
<?php if ($present): ?>
|
||||
<p class="pro-desc"><?= nl2br(htmlspecialchars($present, ENT_QUOTES, 'UTF-8')) ?></p>
|
||||
<p class="pro-desc"><?php echo 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 href="#" class="favori_lien" data-id="<?php echo (int) $p['id'] ?>" data-loading="0">
|
||||
<?php echo 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 href="/public/client/detail_praticien.php?id=<?php echo (int) $p['id'] ?>">
|
||||
<?php echo htmlspecialchars($translations['plus_details'] ?? 'Détails', ENT_QUOTES, 'UTF-8') ?>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Vidéo -->
|
||||
<?php if(!empty($p['video'])): ?>
|
||||
<?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 ?>">
|
||||
data-yturl="<?php echo $href ?>"
|
||||
aria-label="<?php echo $altVid ?>">
|
||||
<?php if ($thumb): ?>
|
||||
<img src="<?= htmlspecialchars($thumb, ENT_QUOTES, 'UTF-8') ?>" alt="<?= $altVid ?>" loading="lazy">
|
||||
<img src="<?php echo htmlspecialchars($thumb, ENT_QUOTES, 'UTF-8') ?>" alt="<?php echo $altVid ?>" loading="lazy">
|
||||
<?php else: ?>
|
||||
<img src="/public/uploads/img/video_placeholder.jpg" alt="<?= $altVid ?>" loading="lazy">
|
||||
<img src="/public/uploads/img/video_placeholder.jpg" alt="<?php echo $altVid ?>" loading="lazy">
|
||||
<?php endif; ?>
|
||||
</a>
|
||||
</div>
|
||||
@@ -220,8 +273,8 @@ function render_results(array $praticiens, array $translations, bool $isSearch,
|
||||
|
||||
<!-- 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 class="pro-cta" href="/public/client/reserver_rdv.php?praticien_id=<?php echo (int) $p['id'] ?>">
|
||||
<?php echo htmlspecialchars($translations['den'] ?? 'Prendre RDV', ENT_QUOTES, 'UTF-8') ?>
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
@@ -229,40 +282,40 @@ function render_results(array $praticiens, array $translations, bool $isSearch,
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if (!$isSearch && $totalPages > 1): ?>
|
||||
<?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">««</a>
|
||||
<a class="pager-arrow <?php echo $page <= 1 ? 'is-disabled' : '' ?>"
|
||||
href="?page=<?php echo max(1, $page - 1) ?>" aria-label="Page précédente">««</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>
|
||||
$start = max(1, $page - 2);
|
||||
$end = min($totalPages, $page + 2);
|
||||
for ($p = $start; $p <= $end; $p++):
|
||||
?>
|
||||
<a class="pager-page <?php echo $p === $page ? 'is-active' : '' ?>" href="?page=<?php echo $p ?>"><?php echo $p ?></a>
|
||||
<?php endfor; ?>
|
||||
|
||||
<a class="pager-arrow <?= $page>=$totalPages?'is-disabled':'' ?>"
|
||||
href="?page=<?= min($totalPages,$page+1) ?>" aria-label="Page suivante">»»</a>
|
||||
<a class="pager-arrow <?php echo $page >= $totalPages ? 'is-disabled' : '' ?>"
|
||||
href="?page=<?php echo min($totalPages, $page + 1) ?>" aria-label="Page suivante">»»</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';
|
||||
?>
|
||||
/* 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>
|
||||
<title><?php echo 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">
|
||||
@@ -295,14 +348,14 @@ require_once '../includes/header.php';
|
||||
<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') ?>">
|
||||
placeholder="<?php echo htmlspecialchars($translations['n_special'] ?? 'spécialité, nom…', ENT_QUOTES, 'UTF-8') ?>"
|
||||
aria-label="spécialité, nom" value="<?php echo 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') ?>">
|
||||
placeholder="<?php echo htmlspecialchars($translations['gde'] ?? 'ville ou code postal', ENT_QUOTES, 'UTF-8') ?>"
|
||||
aria-label="ville ou code postal" value="<?php echo htmlspecialchars($wRaw, ENT_QUOTES, 'UTF-8') ?>">
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -316,7 +369,7 @@ require_once '../includes/header.php';
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p><?= $translations['copyr'] ?></p>
|
||||
<p><?php echo $translations['copyr'] ?></p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -325,8 +378,8 @@ require_once '../includes/header.php';
|
||||
<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>
|
||||
<h5 class="modal-title" id="videoModalLabel"><?php echo htmlspecialchars($translations['video_titre'] ?? 'Vidéo') ?></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="<?php echo htmlspecialchars($translations['fermer'] ?? 'Fermer') ?>"></button>
|
||||
</div>
|
||||
<div class="modal-body pt-0">
|
||||
<div class="ratio ratio-16x9 rounded-3 overflow-hidden">
|
||||
@@ -340,7 +393,7 @@ require_once '../includes/header.php';
|
||||
</div>
|
||||
<div class="modal-footer border-0">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
<?= htmlspecialchars($translations['fermer'] ?? 'Fermer') ?>
|
||||
<?php echo htmlspecialchars($translations['fermer'] ?? 'Fermer') ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -348,6 +401,9 @@ require_once '../includes/header.php';
|
||||
</div>
|
||||
|
||||
<!-- JS -->
|
||||
<script>
|
||||
const csrfToken = <?php echo json_encode(ensure_csrf_token(), JSON_UNESCAPED_UNICODE) ?>;
|
||||
</script>
|
||||
<script>
|
||||
(function(){
|
||||
// Convertit n'importe quelle URL YouTube en URL d'embed nocookie
|
||||
@@ -421,40 +477,69 @@ require_once '../includes/header.php';
|
||||
}, 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'; });
|
||||
function ajouterFavori(el, praticienId) {
|
||||
if (el.dataset.loading === '1') return;
|
||||
el.dataset.loading = '1';
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('praticien_id', String(praticienId));
|
||||
formData.append('csrf_token', csrfToken);
|
||||
|
||||
fetch('/public/client/ajouter_favori.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: formData
|
||||
})
|
||||
.then(async (r) => {
|
||||
const text = await r.text();
|
||||
let data;
|
||||
|
||||
try {
|
||||
data = JSON.parse(text);
|
||||
} catch (e) {
|
||||
throw new Error(text || 'Erreur réseau');
|
||||
}
|
||||
|
||||
if (!r.ok || !data.ok) {
|
||||
throw new Error(data.message || 'Erreur.');
|
||||
}
|
||||
|
||||
el.classList.add('ajoute');
|
||||
el.setAttribute('aria-pressed', 'true');
|
||||
el.title = data.message || '';
|
||||
|
||||
if (typeof Swal !== 'undefined') {
|
||||
Swal.fire({
|
||||
icon: data.already_exists ? 'info' : 'success',
|
||||
title: <?php echo json_encode($translations['sw_favori_ajoute_title'] ?? 'Favori ajouté !', JSON_UNESCAPED_UNICODE) ?>,
|
||||
text: data.message || '',
|
||||
timer: 1800,
|
||||
showConfirmButton: false
|
||||
});
|
||||
} else {
|
||||
alert(data.message || 'OK');
|
||||
}
|
||||
})
|
||||
.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;
|
||||
@@ -462,10 +547,10 @@ require_once '../includes/header.php';
|
||||
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') ?>
|
||||
title: <?php echo json_encode($translations['confirmation'] ?? 'Confirmation') ?>,
|
||||
text: <?php echo json_encode($translations['voulez_vous_ajouter_favori'] ?? 'Ajouter ce praticien à vos favoris ?') ?>,
|
||||
ok: <?php echo json_encode($translations['btn_oui'] ?? 'Oui') ?>,
|
||||
cancel:<?php echo json_encode($translations['btn_annuler'] ?? 'Annuler') ?>
|
||||
};
|
||||
|
||||
if (typeof Swal !== 'undefined') {
|
||||
|
||||
@@ -1,67 +1,68 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../includes/init_client.php';
|
||||
require_once __DIR__ . '/../includes/init_client.php';
|
||||
|
||||
$client_id = $_SESSION['reference_id'];
|
||||
|
||||
|
||||
$client_id = $_SESSION['reference_id'];
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT p.*
|
||||
FROM praticien_client pc
|
||||
JOIN praticien p ON pc.praticien_id = p.id
|
||||
WHERE pc.client_id = ?
|
||||
ORDER BY p.nom ASC
|
||||
");
|
||||
$stmt->execute([$client_id]);
|
||||
$favoris = $stmt->fetchAll();
|
||||
$stmt->execute([$client_id]);
|
||||
$favoris = $stmt->fetchAll();
|
||||
|
||||
// util pour couper les mots de la description texte
|
||||
function h($s)
|
||||
{return htmlspecialchars((string) $s, ENT_QUOTES, 'UTF-8');}
|
||||
function excerpt($text, $len = 160)
|
||||
{
|
||||
$t = trim(strip_tags((string) $text));
|
||||
if (mb_strlen($t, 'UTF-8') <= $len) {
|
||||
return $t;
|
||||
}
|
||||
|
||||
// util pour couper les mots de la description texte
|
||||
function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
|
||||
function excerpt($text, $len=160){
|
||||
$t = trim(strip_tags((string)$text));
|
||||
if (mb_strlen($t,'UTF-8') <= $len) return $t;
|
||||
return mb_substr($t, 0, $len, 'UTF-8') . '…';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
$PAGE_TITLE = $translations['mes_praticiens_favoris_title'] ?? 'Mes praticiens favoris';
|
||||
$HEAD_EXTRA = <<<HTML
|
||||
$PAGE_TITLE = $translations['mes_praticiens_favoris_title'] ?? 'Mes praticiens favoris';
|
||||
$HEAD_EXTRA = <<<HTML
|
||||
<script src="/js/sweetalert2@11.js"></script>
|
||||
HTML;
|
||||
|
||||
require_once '../includes/header.php';
|
||||
require_once '../includes/header.php';
|
||||
?>
|
||||
<div class="main1">
|
||||
<div class="container py-5">
|
||||
<div class="shadow p-4 rounded bg-light">
|
||||
<h2 class="mb-4 text-center"> <?= $translations['mes_praticiens_favoris_heading'] ?></h2>
|
||||
<h2 class="mb-4 text-center"> <?php echo $translations['mes_praticiens_favoris_heading'] ?></h2>
|
||||
|
||||
<?php if (empty($favoris)): ?>
|
||||
<div class="alert alert-info"><?= $translations['aucun_favori_pour_le_moment'] ?></div>
|
||||
<div class="alert alert-info"><?php echo $translations['aucun_favori_pour_le_moment'] ?></div>
|
||||
<?php else: ?>
|
||||
<?php foreach ($favoris as $p): ?>
|
||||
<div class="card mb-4 shadow-sm" id="praticien_<?= $p['id'] ?>">
|
||||
<div class="card mb-4 shadow-sm" id="praticien_<?php echo $p['id'] ?>">
|
||||
<div class="card-body praticien-card">
|
||||
<div>
|
||||
<?php if ($p['photo']): ?>
|
||||
<img src="/public/uploads/<?= htmlspecialchars($p['photo']) ?>" alt="<?= $translations['alt_photo'] ?>" class="card-img-top">
|
||||
<img src="/public/uploads/<?php echo htmlspecialchars($p['photo']) ?>" alt="<?php echo $translations['alt_photo'] ?>" class="card-img-top">
|
||||
<?php else: ?>
|
||||
<img src="/public/uploads/img/user_placeholder.png" alt="<?= $translations['alt_photo'] ?>" class="card-img-top">
|
||||
<img src="/public/uploads/img/user_placeholder.png" alt="<?php echo $translations['alt_photo'] ?>" class="card-img-top">
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<h2 class="pro-name"><?= htmlspecialchars($p['prenom']?? '') . ' ' . htmlspecialchars($p['nom']?? '') ?></h5>
|
||||
<p class="mb-1"><strong><?= $translations['label_ville'] ?> :</strong> <?= htmlspecialchars($p['ville'] ?? '') ?></p>
|
||||
<h2 class="pro-name"><?php echo htmlspecialchars($p['prenom'] ?? '') . ' ' . htmlspecialchars($p['nom'] ?? '') ?></h5>
|
||||
<p class="mb-1"><strong><?php echo $translations['label_ville'] ?> :</strong> <?php echo htmlspecialchars($p['ville'] ?? '') ?></p>
|
||||
<p class="mb-2">
|
||||
<?php if (!empty($p['presentation'])): ?>
|
||||
<?= h(excerpt($p['presentation'], 300)) ?>
|
||||
<?php if (! empty($p['presentation'])): ?>
|
||||
<?php echo h(excerpt($p['presentation'], 300)) ?>
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
|
||||
<div class="d-flex gap-3">
|
||||
<button class="btn btn-danger btn-sm" onclick="supprimerFavori(<?= $p['id'] ?>)"> <?= $translations['btn_retirer'] ?></button>
|
||||
<a class="btn btn-outline-secondary btn-sm btn-table" href="/public/client/reserver_rdv.php?praticien_id=<?= $p['id'] ?>"> <?= $translations['btn_rendez_vous'] ?></a>
|
||||
<button class="btn btn-danger btn-sm" onclick="supprimerFavori(<?php echo $p['id'] ?>)"> <?php echo $translations['btn_retirer'] ?></button>
|
||||
<a class="btn btn-outline-secondary btn-sm btn-table" href="/public/client/reserver_rdv.php?praticien_id=<?php echo $p['id'] ?>"> <?php echo $translations['btn_rendez_vous'] ?></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -70,44 +71,76 @@ require_once '../includes/header.php';
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
const csrfToken = <?php echo json_encode(ensure_csrf_token(), JSON_UNESCAPED_UNICODE) ?>;
|
||||
</script>
|
||||
<script>
|
||||
function supprimerFavori(praticienId) {
|
||||
Swal.fire({
|
||||
title: <?= json_encode($translations['confirm_supprimer_favori_title']) ?>,
|
||||
text: <?= json_encode($translations['confirm_supprimer_favori_text']) ?>,
|
||||
title: <?php echo json_encode($translations['confirm_supprimer_favori_title']) ?>,
|
||||
text: <?php echo json_encode($translations['confirm_supprimer_favori_text']) ?>,
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#dc3545',
|
||||
cancelButtonColor: '#6c757d',
|
||||
confirmButtonText: <?= json_encode($translations['btn_oui_supprimer']) ?>,
|
||||
cancelButtonText: <?= json_encode($translations['btn_annuler']) ?>
|
||||
confirmButtonText: <?php echo json_encode($translations['btn_oui_supprimer']) ?>,
|
||||
cancelButtonText: <?php echo json_encode($translations['btn_annuler']) ?>
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
fetch('/public/client/supprimer_favori.php', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
body: 'praticien_id=' + praticienId
|
||||
})
|
||||
.then(res => res.text())
|
||||
.then(msg => {
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: <?= json_encode($translations['favori_supprime_title']) ?>,
|
||||
text: msg,
|
||||
timer: 1500,
|
||||
showConfirmButton: false
|
||||
});
|
||||
const element = document.getElementById('praticien_' + praticienId);
|
||||
if (element) element.remove();
|
||||
if (!result.isConfirmed) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('praticien_id', String(praticienId));
|
||||
formData.append('csrf_token', csrfToken);
|
||||
|
||||
fetch('/public/client/supprimer_favori.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: formData
|
||||
})
|
||||
.then(async (res) => {
|
||||
const text = await res.text();
|
||||
let data;
|
||||
|
||||
try {
|
||||
data = JSON.parse(text);
|
||||
} catch (e) {
|
||||
throw new Error(text || 'Erreur réseau');
|
||||
}
|
||||
|
||||
if (!res.ok || !data.ok) {
|
||||
throw new Error(data.message || 'Erreur.');
|
||||
}
|
||||
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: <?php echo json_encode($translations['favori_supprime_title']) ?>,
|
||||
text: data.message || '',
|
||||
timer: 1500,
|
||||
showConfirmButton: false
|
||||
});
|
||||
}
|
||||
|
||||
const element = document.getElementById('praticien_' + praticienId);
|
||||
if (element) {
|
||||
element.remove();
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'Oups',
|
||||
text: err.message || 'Erreur.'
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p><?= $translations['copyr'] ?></p>
|
||||
<p><?php echo $translations['copyr'] ?></p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../includes/init_client.php';
|
||||
require_once __DIR__ . '/../includes/init_client.php';
|
||||
|
||||
|
||||
|
||||
$pdo->prepare("
|
||||
$pdo->prepare("
|
||||
INSERT INTO push_badge_counters (compte_id, msg_unread, rdv_unread, updated_at)
|
||||
VALUES (:cid, 0, 0, NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
rdv_unread = 0,
|
||||
updated_at = NOW()
|
||||
")->execute([':cid' => (int)$_SESSION['compte_id']]);
|
||||
")->execute([':cid' => (int) $_SESSION['compte_id']]);
|
||||
|
||||
$client_id = $_SESSION['reference_id'];
|
||||
|
||||
|
||||
$client_id = $_SESSION['reference_id'];
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT
|
||||
rv.*,
|
||||
p.nom AS nom_praticien,
|
||||
@@ -29,143 +25,148 @@ $stmt = $pdo->prepare("
|
||||
WHERE rv.client_id = ? AND rv.date_rdv >= NOW()
|
||||
ORDER BY rv.date_rdv ASC
|
||||
");
|
||||
$stmt->execute([$client_id]);
|
||||
$rdvs = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
$stmt->execute([$client_id]);
|
||||
$rdvs = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$PAGE_TITLE = $translations['mes_rdv_title'] ?? 'Mes rendez-vous';
|
||||
$HEAD_EXTRA = <<<HTML
|
||||
$PAGE_TITLE = $translations['mes_rdv_title'] ?? 'Mes rendez-vous';
|
||||
$HEAD_EXTRA = <<<HTML
|
||||
<script src="/js/sweetalert2@11.js"></script>
|
||||
HTML;
|
||||
require_once '../includes/header.php';
|
||||
require_once '../includes/header.php';
|
||||
?>
|
||||
<div class="main1">
|
||||
<div class="container py-5">
|
||||
<div class="shadow p-4 rounded bg-light">
|
||||
<h2 class="mb-4 text-center">📅 <?= $translations['mes_rdv_heading'] ?></h2>
|
||||
<h2 class="mb-4 text-center">📅 <?php echo $translations['mes_rdv_heading'] ?></h2>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover align-middle mobile-cards" id="table_rdv">
|
||||
<thead class="table-light">
|
||||
<tr align="center">
|
||||
<th><?= $translations['col_date'] ?></th>
|
||||
<th><?= $translations['col_heure'] ?></th>
|
||||
<th><?= $translations['col_praticien'] ?></th>
|
||||
<th><?= $translations['col_duree'] ?></th>
|
||||
<th><?= $translations['col_statut'] ?></th>
|
||||
<th><?= $translations['col_action'] ?></th>
|
||||
<th><?php echo $translations['col_date'] ?></th>
|
||||
<th><?php echo $translations['col_heure'] ?></th>
|
||||
<th><?php echo $translations['col_praticien'] ?></th>
|
||||
<th><?php echo $translations['col_duree'] ?></th>
|
||||
<th><?php echo $translations['col_statut'] ?></th>
|
||||
<th><?php echo $translations['col_action'] ?></th>
|
||||
<!-- Chat -->
|
||||
<th><?= $translations['btn_chat'] ?></th>
|
||||
<th><?php echo $translations['btn_chat'] ?></th>
|
||||
<!-- Visio / Adresse -->
|
||||
<th><?= htmlspecialchars($translations['status_online']) ?></th>
|
||||
<th><?php echo htmlspecialchars($translations['status_online']) ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($rdvs)): ?>
|
||||
<tr><td colspan="8" class="text-center"><?= $translations['aucun_rdv_a_venir'] ?></td></tr>
|
||||
<tr><td colspan="8" class="text-center"><?php echo $translations['aucun_rdv_a_venir'] ?></td></tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($rdvs as $r):
|
||||
$dt = new DateTime($r['date_rdv']);
|
||||
$id = (int)$r['id'];
|
||||
$ficheUrl = '/specialiste?id='.(int)$r['praticien_id'];
|
||||
$dt = new DateTime($r['date_rdv']);
|
||||
$id = (int) $r['id'];
|
||||
$ficheUrl = '/specialiste?id=' . (int) $r['praticien_id'];
|
||||
?>
|
||||
<tr>
|
||||
<td><?= $dt->format('d/m/Y') ?></td>
|
||||
<td><?= $dt->format('H:i') ?></td>
|
||||
<td><?php echo $dt->format('d/m/Y') ?></td>
|
||||
<td><?php echo $dt->format('H:i') ?></td>
|
||||
<td>
|
||||
<a href="/public/client/detail_praticien.php?id=<?= (int)$r['praticien_id'] ?>">
|
||||
<?= htmlspecialchars($r['prenom_praticien'] . ' ' . $r['nom_praticien']) ?>
|
||||
<a href="/public/client/detail_praticien.php?id=<?php echo (int) $r['praticien_id'] ?>">
|
||||
<?php echo htmlspecialchars($r['prenom_praticien'] . ' ' . $r['nom_praticien']) ?>
|
||||
</a>
|
||||
</td>
|
||||
<td><?= (int)$r['duree_minutes'] ?> <?= $translations['min_abbr'] ?></td>
|
||||
<td><?php echo (int) $r['duree_minutes'] ?> <?php echo $translations['min_abbr'] ?></td>
|
||||
|
||||
<td id="statut_<?= $id ?>">
|
||||
<span class="badge rounded-pill <?= $r['statut'] === 'confirmé' ? 'bg-success' : ($r['statut'] === 'prévu' ? 'bg-warning text-dark' : 'bg-secondary') ?>">
|
||||
<?= $r['statut'] === 'confirmé' ? $translations['statut_confirme'] : ($r['statut'] === 'prévu' ? $translations['statut_prevu'] : $translations['statut_annule']) ?>
|
||||
<td id="statut_<?php echo $id ?>">
|
||||
<span class="badge rounded-pill <?php echo $r['statut'] === 'confirmé' ? 'bg-success' : ($r['statut'] === 'prévu' ? 'bg-warning text-dark' : 'bg-secondary') ?>">
|
||||
<?php echo $r['statut'] === 'confirmé' ? $translations['statut_confirme'] : ($r['statut'] === 'prévu' ? $translations['statut_prevu'] : $translations['statut_annule']) ?>
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<?php if ($r['statut'] !== 'annulé'): ?>
|
||||
<button class="btn btn-outline-danger btn-sm" onclick="annulerRdv(<?= $id ?>)"> <?= $translations['btn_annuler'] ?></button>
|
||||
<button class="btn btn-outline-danger btn-sm" onclick="annulerRdv(<?php echo $id ?>)"> <?php echo $translations['btn_annuler'] ?></button>
|
||||
<?php else: ?>
|
||||
<em class="text-muted"><?= $translations['statut_annule'] ?></em>
|
||||
<em class="text-muted"><?php echo $translations['statut_annule'] ?></em>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
|
||||
<!-- Chat -->
|
||||
<td>
|
||||
<a class="btn btn-outline-secondary btn-sm btn-table" href="/messages/start.php?praticien_id=<?= (int)$r['praticien_id'] ?>">
|
||||
<?= $translations['chat_praticien'] ?>
|
||||
<a class="btn btn-outline-secondary btn-sm btn-table" href="/messages/start.php?praticien_id=<?php echo (int) $r['praticien_id'] ?>">
|
||||
<?php echo $translations['chat_praticien'] ?>
|
||||
</a>
|
||||
</td>
|
||||
|
||||
<!-- Visio (online) OU Adresse/Lien (offline) -->
|
||||
<td>
|
||||
<?php
|
||||
$lieu = strtolower(trim((string)($r['lieu'] ?? '')));
|
||||
if ($lieu === 'online') {
|
||||
// --- Affichage visio comme avant ---
|
||||
$statut = $r['statut']; // 'confirmé' | 'prévu' | 'annulé'
|
||||
$lieu = strtolower(trim((string) ($r['lieu'] ?? '')));
|
||||
if ($lieu === 'online') {
|
||||
// --- Affichage visio comme avant ---
|
||||
$statut = $r['statut']; // 'confirmé' | 'prévu' | 'annulé'
|
||||
|
||||
$tzParis = new DateTimeZone('Europe/Paris');
|
||||
$now = new DateTime('now', $tzParis);
|
||||
$start = new DateTime($r['date_rdv'], $tzParis);
|
||||
$open = (clone $start)->modify('-15 minutes');
|
||||
$close = (clone $start)->modify('+'.(int)($r['duree_minutes'] ?? 120).' minutes');
|
||||
$tzParis = new DateTimeZone('Europe/Paris');
|
||||
$now = new DateTime('now', $tzParis);
|
||||
$start = new DateTime($r['date_rdv'], $tzParis);
|
||||
$open = (clone $start)->modify('-15 minutes');
|
||||
$close = (clone $start)->modify('+' . (int) ($r['duree_minutes'] ?? 120) . ' minutes');
|
||||
|
||||
if ($statut === 'confirmé') {
|
||||
if ($now >= $open && $now <= $close) {
|
||||
// Bouton actif
|
||||
?>
|
||||
<a href="/visio/join.php?rdv=<?= (int)$r['id'] ?>&start=1" class="btn btn-outline-secondary btn-sm btn-table">
|
||||
<?= htmlspecialchars($translations['visio_nachalo']) ?>
|
||||
if ($statut === 'confirmé') {
|
||||
if ($now >= $open && $now <= $close) {
|
||||
// Bouton actif
|
||||
?>
|
||||
<a href="/visio/join.php?rdv=<?php echo (int) $r['id'] ?>&start=1" class="btn btn-outline-secondary btn-sm btn-table">
|
||||
<?php echo htmlspecialchars($translations['visio_nachalo']) ?>
|
||||
</a>
|
||||
<?php
|
||||
} else {
|
||||
// Messages hors fenêtre
|
||||
?>
|
||||
} else {
|
||||
// Messages hors fenêtre
|
||||
?>
|
||||
<small class="text-muted d-block mt-1">
|
||||
<?php
|
||||
if ($now < $open) echo htmlspecialchars(($translations['ouvert_a'] ?? 'Откроется в')).' '.$open->format('H:i');
|
||||
elseif ($now > $close) echo htmlspecialchars(($translations['ferme_a'] ?? 'Закрыто в')).' '.$close->format('H:i');
|
||||
else echo htmlspecialchars(($translations['prev_a'] ?? 'Запланировано на')).' '.$start->format('d/m/Y \à H:i').'.';
|
||||
?>
|
||||
if ($now < $open) {
|
||||
echo htmlspecialchars(($translations['ouvert_a'] ?? 'Откроется в')) . ' ' . $open->format('H:i');
|
||||
} elseif ($now > $close) {
|
||||
echo htmlspecialchars(($translations['ferme_a'] ?? 'Закрыто в')) . ' ' . $close->format('H:i');
|
||||
} else {
|
||||
echo htmlspecialchars(($translations['prev_a'] ?? 'Запланировано на')) . ' ' . $start->format('d/m/Y \à H:i') . '.';
|
||||
}
|
||||
|
||||
?>
|
||||
</small>
|
||||
<?php
|
||||
}
|
||||
} elseif ($statut === 'prévu') {
|
||||
// Pas encore confirmé
|
||||
?>
|
||||
}
|
||||
} elseif ($statut === 'prévu') {
|
||||
// Pas encore confirmé
|
||||
?>
|
||||
<small class="text-danger d-block mt-1">
|
||||
<?= htmlspecialchars($translations['votre_rdv_jdet'] ?? 'En attente de confirmation') ?>
|
||||
<?php echo htmlspecialchars($translations['votre_rdv_jdet'] ?? 'En attente de confirmation') ?>
|
||||
</small>
|
||||
<?php
|
||||
} else {
|
||||
?>
|
||||
} else {
|
||||
?>
|
||||
<small class="text-muted d-block mt-1">
|
||||
<?= htmlspecialchars($translations['statut_annule'] ?? 'Annulé') ?>
|
||||
<?php echo htmlspecialchars($translations['statut_annule'] ?? 'Annulé') ?>
|
||||
</small>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
// --- OFFLINE : afficher adresse (si dispo) + lien fiche praticien ---
|
||||
$addrParts = array_filter([
|
||||
trim((string)($r['p_adresse'] ?? '')),
|
||||
trim((string)($r['p_cp'] ?? '')) . ' ' . trim((string)($r['p_ville'] ?? ''))
|
||||
], fn($x) => $x !== '' && $x !== ' ');
|
||||
$addr = trim(implode(', ', $addrParts));
|
||||
} else {
|
||||
// --- OFFLINE : afficher adresse (si dispo) + lien fiche praticien ---
|
||||
$addrParts = array_filter([
|
||||
trim((string) ($r['p_adresse'] ?? '')),
|
||||
trim((string) ($r['p_cp'] ?? '')) . ' ' . trim((string) ($r['p_ville'] ?? '')),
|
||||
], fn($x) => $x !== '' && $x !== ' ');
|
||||
$addr = trim(implode(', ', $addrParts));
|
||||
|
||||
if ($addr !== '') {
|
||||
//echo '<div class="mb-1"> '.htmlspecialchars($addr).'</div>';
|
||||
}
|
||||
?>
|
||||
<a href="<?= htmlspecialchars($ficheUrl) ?>" class="btn btn-outline-secondary btn-sm btn-table">
|
||||
<?= htmlspecialchars($translations['voir_fiche_praticien'] ?? 'Voir la fiche') ?>
|
||||
if ($addr !== '') {
|
||||
//echo '<div class="mb-1"> '.htmlspecialchars($addr).'</div>';
|
||||
}
|
||||
?>
|
||||
<a href="<?php echo htmlspecialchars($ficheUrl) ?>" class="btn btn-outline-secondary btn-sm btn-table">
|
||||
<?php echo htmlspecialchars($translations['voir_fiche_praticien'] ?? 'Voir la fiche') ?>
|
||||
</a>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
}
|
||||
?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
@@ -175,32 +176,35 @@ require_once '../includes/header.php';
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const csrfToken = <?php echo json_encode(ensure_csrf_token(), JSON_UNESCAPED_UNICODE) ?>;
|
||||
function annulerRdv(id) {
|
||||
Swal.fire({
|
||||
title: <?= json_encode($translations['confirm_annulation_title']) ?>,
|
||||
title: <?php echo json_encode($translations['confirm_annulation_title']) ?>,
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: <?= json_encode($translations['btn_oui_annuler']) ?>,
|
||||
cancelButtonText: <?= json_encode($translations['btn_annuler']) ?>,
|
||||
confirmButtonText: <?php echo json_encode($translations['btn_oui_annuler']) ?>,
|
||||
cancelButtonText: <?php echo json_encode($translations['btn_annuler']) ?>,
|
||||
confirmButtonColor: '#dc3545',
|
||||
cancelButtonColor: '#6c757d'
|
||||
}).then((result) => {
|
||||
if (!result.isConfirmed) return;
|
||||
|
||||
fetch('/public/client/annuler_rdv.php', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'},
|
||||
body: new URLSearchParams({ id: String(id) })
|
||||
})
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'},
|
||||
body: new URLSearchParams({
|
||||
id: String(id),
|
||||
csrf_token: csrfToken
|
||||
})
|
||||
})
|
||||
.then(r => r.json().catch(() => ({ok:false, message:'Bad JSON'})))
|
||||
.then(data => {
|
||||
if (data.ok) {
|
||||
Swal.fire({ icon:'success', title: <?= json_encode($translations['rdv_annule_ok']) ?>, text: data.message, timer: 1600, showConfirmButton:false });
|
||||
Swal.fire({ icon:'success', title: <?php echo json_encode($translations['rdv_annule_ok']) ?>, text: data.message, timer: 1600, showConfirmButton:false });
|
||||
const statutEl = document.getElementById("statut_" + id);
|
||||
if (statutEl) {
|
||||
statutEl.innerHTML = '<span class="badge bg-secondary"><?= htmlspecialchars($translations['statut_annule']) ?></span>';
|
||||
statutEl.innerHTML = '<span class="badge bg-secondary"><?php echo htmlspecialchars($translations['statut_annule']) ?></span>';
|
||||
}
|
||||
} else {
|
||||
Swal.fire({ icon:'error', title: 'Oops', text: data.message || 'Erreur' });
|
||||
@@ -241,7 +245,7 @@ function annulerRdv(id) {
|
||||
</div>
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p><?= $translations['copyr'] ?></p>
|
||||
<p><?php echo $translations['copyr'] ?></p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
|
||||
+48
-46
@@ -1,19 +1,20 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../includes/init_client.php';
|
||||
require_once __DIR__ . '/../includes/init_client.php';
|
||||
|
||||
$id = (int)($_SESSION['reference_id'] ?? 0);
|
||||
if (!$id) { http_response_code(403); exit($translations['client_introuvable'] ?? 'Client introuvable'); }
|
||||
$id = (int) ($_SESSION['reference_id'] ?? 0);
|
||||
if (! $id) {http_response_code(403);exit($translations['client_introuvable'] ?? 'Client introuvable');}
|
||||
|
||||
// Récupérer les données du client
|
||||
$stmt = $pdo->prepare("SELECT * FROM client WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$client = $stmt->fetch();
|
||||
if (!$client) {
|
||||
// Récupérer les données du client
|
||||
$stmt = $pdo->prepare("SELECT * FROM client WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$client = $stmt->fetch();
|
||||
if (! $client) {
|
||||
exit($translations['client_introuvable'] ?? 'Client introuvable');
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Traitement du formulaire AVANT tout output ----
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
|
||||
// ---- Traitement du formulaire AVANT tout output ----
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
|
||||
require_post_csrf();
|
||||
$nom = trim($_POST['nom'] ?? '');
|
||||
$prenom = trim($_POST['prenom'] ?? '');
|
||||
$telephone = trim($_POST['telephone'] ?? '');
|
||||
@@ -30,84 +31,85 @@ if (($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
|
||||
WHERE id=:id
|
||||
");
|
||||
$stmt->execute([
|
||||
'nom' => $nom,
|
||||
'prenom' => $prenom,
|
||||
'telephone' => $telephone,
|
||||
'genre' => $genre,
|
||||
'nom' => $nom,
|
||||
'prenom' => $prenom,
|
||||
'telephone' => $telephone,
|
||||
'genre' => $genre,
|
||||
'date_naissance' => $date_naiss ?: null,
|
||||
'adresse' => $adresse,
|
||||
'ville' => $ville,
|
||||
'code_postal' => $cp,
|
||||
'id' => $id
|
||||
'adresse' => $adresse,
|
||||
'ville' => $ville,
|
||||
'code_postal' => $cp,
|
||||
'id' => $id,
|
||||
]);
|
||||
|
||||
// redirection (évite le repost + actualise la page infos)
|
||||
header('Location: /public/client/informations.php?updated=1');
|
||||
exit;
|
||||
}
|
||||
require_once __DIR__ . '/../includes/header.php';
|
||||
}
|
||||
require_once __DIR__ . '/../includes/header.php';
|
||||
|
||||
?>
|
||||
|
||||
<div class="main1">
|
||||
<div class="container py-5">
|
||||
|
||||
|
||||
|
||||
<form method="POST" class="shadow p-4 rounded bg-light">
|
||||
<h2 class="mb-4 text-center"><?= $translations['modifier_infos_heading'] ?></h2>
|
||||
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(ensure_csrf_token(), ENT_QUOTES, 'UTF-8') ?>">
|
||||
<h2 class="mb-4 text-center"><?php echo $translations['modifier_infos_heading'] ?></h2>
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= $translations['label_nom'] ?> *</label>
|
||||
<input type="text" name="nom" value="<?= htmlspecialchars($client['nom'] ?? '') ?>" required class="form-control">
|
||||
<label class="form-label"><?php echo $translations['label_nom'] ?> *</label>
|
||||
<input type="text" name="nom" value="<?php echo htmlspecialchars($client['nom'] ?? '') ?>" required class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= $translations['label_prenom'] ?> *</label>
|
||||
<input type="text" name="prenom" value="<?= htmlspecialchars($client['prenom'] ?? '') ?>" required class="form-control">
|
||||
<label class="form-label"><?php echo $translations['label_prenom'] ?> *</label>
|
||||
<input type="text" name="prenom" value="<?php echo htmlspecialchars($client['prenom'] ?? '') ?>" required class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= $translations['label_telephone'] ?> *</label>
|
||||
<input type="tel" name="telephone" id="telephone" value="<?= htmlspecialchars($client['telephone'] ?? '') ?>" class="form-control" placeholder="+33600000000"
|
||||
<label class="form-label"><?php echo $translations['label_telephone'] ?> *</label>
|
||||
<input type="tel" name="telephone" id="telephone" value="<?php echo htmlspecialchars($client['telephone'] ?? '') ?>" class="form-control" placeholder="+33600000000"
|
||||
required
|
||||
pattern="^\+?[1-9]\d{6,14}$"
|
||||
title="<?=htmlspecialchars($translations['telephone_format_message']?? '') ?>">
|
||||
title="<?php echo htmlspecialchars($translations['telephone_format_message'] ?? '') ?>">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= $translations['label_genre'] ?></label>
|
||||
<label class="form-label"><?php echo $translations['label_genre'] ?></label>
|
||||
<select name="genre" class="form-select">
|
||||
<option value=""><?= $translations['select_selectionnez'] ?></option>
|
||||
<option value="<?= $translations['genre_homme'] ?>" <?= $client['genre'] === $translations['genre_homme'] ? 'selected' : '' ?>><?= $translations['genre_homme'] ?></option>
|
||||
<option value="<?= $translations['genre_femme'] ?>" <?= $client['genre'] === $translations['genre_femme'] ? 'selected' : '' ?>><?= $translations['genre_femme'] ?></option>
|
||||
<option value="<?= $translations['genre_autre'] ?>" <?= $client['genre'] === $translations['genre_autre'] ? 'selected' : '' ?>><?= $translations['genre_autre'] ?></option>
|
||||
<option value=""><?php echo $translations['select_selectionnez'] ?></option>
|
||||
<option value="<?php echo $translations['genre_homme'] ?>" <?php echo $client['genre'] === $translations['genre_homme'] ? 'selected' : '' ?>><?php echo $translations['genre_homme'] ?></option>
|
||||
<option value="<?php echo $translations['genre_femme'] ?>" <?php echo $client['genre'] === $translations['genre_femme'] ? 'selected' : '' ?>><?php echo $translations['genre_femme'] ?></option>
|
||||
<option value="<?php echo $translations['genre_autre'] ?>" <?php echo $client['genre'] === $translations['genre_autre'] ? 'selected' : '' ?>><?php echo $translations['genre_autre'] ?></option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= $translations['label_date_naissance'] ?></label>
|
||||
<input type="date" name="date_naissance" value="<?= htmlspecialchars($client['date_naissance'] ?? '') ?>" class="form-control">
|
||||
<label class="form-label"><?php echo $translations['label_date_naissance'] ?></label>
|
||||
<input type="date" name="date_naissance" value="<?php echo htmlspecialchars($client['date_naissance'] ?? '') ?>" class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="mb-3 input-wrapper">
|
||||
<label class="form-label"><?= $translations['label_adresse'] ?></label>
|
||||
<textarea name="adresse" id="adresse" class="form-control"><?= htmlspecialchars($client['adresse'] ?? '') ?></textarea>
|
||||
<label class="form-label"><?php echo $translations['label_adresse'] ?></label>
|
||||
<textarea name="adresse" id="adresse" class="form-control"><?php echo htmlspecialchars($client['adresse'] ?? '') ?></textarea>
|
||||
<ul id="suggestions" class="list-group position-absolute"></ul>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= $translations['label_ville'] ?></label>
|
||||
<input type="text" name="ville" id="ville" value="<?= htmlspecialchars($client['ville'] ?? '') ?>" class="form-control">
|
||||
<label class="form-label"><?php echo $translations['label_ville'] ?></label>
|
||||
<input type="text" name="ville" id="ville" value="<?php echo htmlspecialchars($client['ville'] ?? '') ?>" class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="form-label"><?= $translations['label_code_postal'] ?></label>
|
||||
<input type="text" name="code_postal" id="code_postal" value="<?= htmlspecialchars($client['code_postal'] ?? '') ?>" class="form-control">
|
||||
<label class="form-label"><?php echo $translations['label_code_postal'] ?></label>
|
||||
<input type="text" name="code_postal" id="code_postal" value="<?php echo htmlspecialchars($client['code_postal'] ?? '') ?>" class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<button type="submit" class="btn btn-primary"><?= $translations['btn_mettre_a_jour'] ?></button>
|
||||
<button type="submit" class="btn btn-primary"><?php echo $translations['btn_mettre_a_jour'] ?></button>
|
||||
</div>
|
||||
<?= $translations['champs_obligatoire'] ?>
|
||||
<?php echo $translations['champs_obligatoire'] ?>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -217,7 +219,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
</div>
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p><?= $translations['copyr'] ?></p>
|
||||
<p><?php echo $translations['copyr'] ?></p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
|
||||
@@ -1,15 +1,59 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
|
||||
require_once __DIR__ . '/../includes/init_client.php';
|
||||
|
||||
$client_id = $_SESSION['reference_id'];
|
||||
$praticien_id = intval($_POST['praticien_id'] ?? 0);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
if ($praticien_id > 0) {
|
||||
|
||||
$stmt = $pdo->prepare("DELETE FROM praticien_client WHERE client_id = ? AND praticien_id = ?");
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'message' => 'Méthode non autorisée.',
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
require_post_csrf();
|
||||
|
||||
$client_id = (int) ($_SESSION['reference_id'] ?? 0);
|
||||
$praticien_id = (int) ($_POST['praticien_id'] ?? 0);
|
||||
|
||||
if ($client_id <= 0) {
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'message' => $translations['erreur_generique'] ?? 'Erreur.',
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($praticien_id <= 0) {
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'message' => $translations['praticien_id_invalide'] ?? 'ID praticien invalide.',
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = $pdo->prepare("
|
||||
DELETE FROM praticien_client
|
||||
WHERE client_id = ? AND praticien_id = ?
|
||||
");
|
||||
$stmt->execute([$client_id, $praticien_id]);
|
||||
echo $translations['favori_supprime_title'];
|
||||
} else {
|
||||
echo $translations['praticien_id_invalide'];
|
||||
|
||||
echo json_encode([
|
||||
'ok' => true,
|
||||
'deleted' => ($stmt->rowCount() > 0),
|
||||
'message' => $translations['favori_supprime_title'] ?? 'Favori supprimé.',
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
|
||||
} catch (Throwable $e) {
|
||||
error_log('supprimer_favori.php: ' . $e->getMessage());
|
||||
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'message' => $translations['erreur_generique'] ?? 'Erreur serveur.',
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
@@ -292,9 +292,14 @@
|
||||
<li onclick="closeMenu(); openContactAdmin()">
|
||||
<?php echo htmlspecialchars($translations['contact_admin']) ?>
|
||||
</li>
|
||||
<li onclick="location.href='/public/deconnexion.php'">
|
||||
<?php echo htmlspecialchars($translations['menu_deconnexion']) ?>
|
||||
</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>
|
||||
|
||||
@@ -1,59 +1,58 @@
|
||||
<?php
|
||||
// ⚠️ NE PAS inclure contact_admin.php ici (risque d'exécution à l'inclusion)
|
||||
require_once __DIR__ . '/../contact_admin.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);
|
||||
|
||||
$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'] ?? ''));
|
||||
|
||||
// =====================================================
|
||||
// [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'
|
||||
);
|
||||
|
||||
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'];
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// [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);
|
||||
}
|
||||
}
|
||||
|
||||
$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;
|
||||
|
||||
// =====================================================
|
||||
// [Liste Praticien avec autorisations]
|
||||
// =====================================================
|
||||
$pid = (int)$currentPraticienId;
|
||||
$permRow = null;
|
||||
|
||||
if ($pid && $sec_uid) {
|
||||
$q = $pdo->prepare("
|
||||
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
|
||||
@@ -61,48 +60,50 @@ if ($pid && $sec_uid) {
|
||||
AND sp.is_active = 1
|
||||
LIMIT 1
|
||||
");
|
||||
$q->execute(['u' => $sec_uid, 'pid' => $pid]);
|
||||
$permRow = $q->fetch(PDO::FETCH_ASSOC) ?: null;
|
||||
}
|
||||
$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;
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// [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';
|
||||
}
|
||||
$fallbackBack = $safeRef ?: '/public/cabinet/dashboard.php';
|
||||
|
||||
// =====================================================
|
||||
// [DETECTION MOBILE]
|
||||
// =====================================================
|
||||
$is_mobile = preg_match('/Mobile|Android|iPhone|iPad/i', $_SERVER['HTTP_USER_AGENT'] ?? '');
|
||||
// =====================================================
|
||||
// [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 ?? '' ?>
|
||||
<title><?php echo htmlspecialchars($PAGE_TITLE ?? 'Cabinet', ENT_QUOTES, 'UTF-8') ?></title>
|
||||
<?php echo $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') ?>">
|
||||
<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/admin_style.css') ?>">
|
||||
<link rel="stylesheet" href="<?php echo asset('/includes/mobil_style.css') ?>">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@@ -113,11 +114,11 @@ $is_mobile = preg_match('/Mobile|Android|iPhone|iPad/i', $_SERVER['HTTP_USER_AGE
|
||||
<div class="header-left">
|
||||
<div class="brand">
|
||||
<a
|
||||
href="<?= htmlspecialchars($fallbackBack, ENT_QUOTES, 'UTF-8') ?>"
|
||||
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="<?= htmlspecialchars($translations['retour'] ?? 'Retour', ENT_QUOTES, 'UTF-8') ?>"
|
||||
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"/>
|
||||
@@ -144,9 +145,9 @@ $is_mobile = preg_match('/Mobile|Android|iPhone|iPad/i', $_SERVER['HTTP_USER_AGE
|
||||
<!-- ⚡ 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 echo 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>
|
||||
<span class="text-muted"><?php echo htmlspecialchars($translations['aucun_praticien_selectionne'] ?? 'Praticien non sélectionné', ENT_QUOTES, 'UTF-8') ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
@@ -163,38 +164,43 @@ $is_mobile = preg_match('/Mobile|Android|iPhone|iPad/i', $_SERVER['HTTP_USER_AGE
|
||||
<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') ?>
|
||||
<?php echo 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') ?>
|
||||
<?php if (! empty($permRow['can_manage_rdv'])): ?>
|
||||
<li onclick="location.href='/public/cabinet/agenda.php?praticien_id=<?php echo $pid ?>'">
|
||||
<?php echo 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 onclick="location.href='/public/cabinet/rendez_vous_du_jour.php?praticien_id=<?php echo $pid ?>'">
|
||||
<?php echo 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 onclick="location.href='/public/cabinet/rendez_vous.php?praticien_id=<?php echo $pid ?>'">
|
||||
<?php echo 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 onclick="location.href='/public/cabinet/reserver_rdv.php?praticien_id=<?php echo $pid ?>'">
|
||||
<?php echo 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') ?>
|
||||
<?php if (! empty($permRow['can_edit_slots'])): ?>
|
||||
<li onclick="location.href='/public/cabinet/disponibilites.php?praticien_id=<?php echo $pid ?>'">
|
||||
<?php echo 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') ?>
|
||||
<?php echo htmlspecialchars($translations['contact_admin'] ?? '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>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
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'])) {
|
||||
@@ -8,16 +8,18 @@ if (isset($_SERVER['SCRIPT_FILENAME']) && realpath(__FILE__) === realpath($_SERV
|
||||
}
|
||||
|
||||
/** Est-ce que l'utilisateur est connecté ? */
|
||||
function is_logged_in(): bool {
|
||||
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);
|
||||
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'] ?? '/');
|
||||
@@ -26,59 +28,78 @@ function enforce_session_timeout(int $maxIdleSeconds = 7200): void {
|
||||
}
|
||||
|
||||
/** 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;
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
function logout_and_redirect_to_login(): 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) {
|
||||
|
||||
setcookie(session_name(), '', [
|
||||
'expires' => time() - 42000,
|
||||
'path' => $params['path'] ?? '/',
|
||||
'domain' => $params['domain'] ?? '',
|
||||
'secure' => (bool) ($params['secure'] ?? false),
|
||||
'httponly' => (bool) ($params['httponly'] ?? true),
|
||||
'samesite' => $params['samesite'] ?? 'Lax',
|
||||
]);
|
||||
|
||||
session_destroy();
|
||||
}
|
||||
redirect_to_login_with_next($requestUri ?? ($_SERVER['REQUEST_URI'] ?? '/'));
|
||||
|
||||
header('Location: /index.php', true, 303);
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔒 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 '';
|
||||
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() ?: '');
|
||||
$role = (string) ($stmt->fetchColumn() ?: '');
|
||||
if ($role !== '') {
|
||||
$_SESSION['role'] = $role;
|
||||
}
|
||||
@@ -93,26 +114,27 @@ function refresh_role_from_db(PDO $pdo): string {
|
||||
* - '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()) {
|
||||
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);
|
||||
$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'] ?? ''));
|
||||
$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);
|
||||
$allowed = array_map(static fn($r) => strtolower((string) $r), $allowedRoles);
|
||||
|
||||
if (!in_array($userRole, $allowed, true)) {
|
||||
if (! in_array($userRole, $allowed, true)) {
|
||||
redirect_forbidden($_SERVER['REQUEST_URI'] ?? '/');
|
||||
}
|
||||
|
||||
@@ -120,8 +142,8 @@ function require_login_or_redirect(?array $allowedRoles = null, array $options =
|
||||
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) {
|
||||
$refId = (int) ($_SESSION['reference_id'] ?? 0);
|
||||
if ($refId !== (int) $strictRef) {
|
||||
redirect_forbidden($_SERVER['REQUEST_URI'] ?? '/');
|
||||
}
|
||||
}
|
||||
@@ -134,35 +156,40 @@ function require_login_or_redirect(?array $allowedRoles = null, array $options =
|
||||
========================= */
|
||||
|
||||
/** Token CSRF session */
|
||||
function ensure_csrf_token(): string {
|
||||
function ensure_csrf_token(): string
|
||||
{
|
||||
if (empty($_SESSION['csrf_token'])) {
|
||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
return (string)$_SESSION['csrf_token'];
|
||||
return (string) $_SESSION['csrf_token'];
|
||||
}
|
||||
|
||||
/** Exige POST + CSRF (supporte aussi header X-CSRF-Token si tu veux fetch/AJAX) */
|
||||
function require_post_csrf(): void {
|
||||
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 === '' && ! empty($_SERVER['HTTP_X_CSRF_TOKEN'])) {
|
||||
$token = (string) $_SERVER['HTTP_X_CSRF_TOKEN'];
|
||||
}
|
||||
|
||||
if ($token === '' || !hash_equals((string)($_SESSION['csrf_token'] ?? ''), (string)$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 {
|
||||
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);
|
||||
if (strlen($details) > 2000) {
|
||||
$details = substr($details, 0, 2000);
|
||||
}
|
||||
|
||||
$pdo->prepare(
|
||||
"INSERT INTO admin_actions_log (user_id, action, details, ip) VALUES (?,?,?,?)"
|
||||
@@ -170,6 +197,6 @@ function log_admin_action(PDO $pdo, int $userId, string $action, string $details
|
||||
$userId,
|
||||
$action,
|
||||
$details,
|
||||
$_SERVER['REMOTE_ADDR'] ?? ''
|
||||
$_SERVER['REMOTE_ADDR'] ?? '',
|
||||
]);
|
||||
}
|
||||
+1758
-1593
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
+1767
-1601
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
+55
-40
@@ -1,37 +1,41 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../includes/init_praticien.php';
|
||||
require_once __DIR__ . '/../includes/init_praticien.php';
|
||||
|
||||
$success = $erreur = '';
|
||||
$id = $_SESSION['compte_id'];
|
||||
$success = $erreur = '';
|
||||
$id = $_SESSION['compte_id'];
|
||||
|
||||
// Récupérer e-mail actuel
|
||||
$stmt = $pdo->prepare("SELECT email FROM compte WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$compte = $stmt->fetch();
|
||||
// Récupérer e-mail actuel
|
||||
$stmt = $pdo->prepare("SELECT email, mot_de_passe FROM compte WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$compte = $stmt->fetch();
|
||||
|
||||
if (!$compte) {
|
||||
if (! $compte) {
|
||||
die($translations['compte_introuvable']);
|
||||
}
|
||||
}
|
||||
|
||||
$email_actuel = $compte['email'];
|
||||
$email_actuel = $compte['email'];
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
require_post_csrf();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$nouvel_email = trim($_POST['email'] ?? '');
|
||||
$mdp1 = $_POST['mot_de_passe'] ?? '';
|
||||
$mdp2 = $_POST['confirm_mot_de_passe'] ?? '';
|
||||
$mdpActuel = $_POST['mot_de_passe_actuel'] ?? '';
|
||||
$mdp1 = $_POST['mot_de_passe'] ?? '';
|
||||
$mdp2 = $_POST['confirm_mot_de_passe'] ?? '';
|
||||
|
||||
if (!filter_var($nouvel_email, FILTER_VALIDATE_EMAIL)) {
|
||||
if (! password_verify($mdpActuel, (string) ($compte['mot_de_passe'] ?? ''))) {
|
||||
$erreur = $translations['erreur_mdp_actuel_incorrect'] ?? 'Mot de passe actuel incorrect.';
|
||||
} elseif (! filter_var($nouvel_email, FILTER_VALIDATE_EMAIL)) {
|
||||
$erreur = $translations['erreur_email_non_valide'];
|
||||
} elseif ($mdp1 && ($mdp1 !== $mdp2 || strlen($mdp1) < 8)) {
|
||||
$erreur = $translations['erreur_mdp_invalides'];
|
||||
} else {
|
||||
// Vérifier si l'email est déjà pris (sauf si c’est le sien)
|
||||
$stmt = $pdo->prepare("SELECT id FROM compte WHERE email = ? AND id != ?");
|
||||
$stmt->execute([$nouvel_email, $id]);
|
||||
|
||||
if ($stmt->fetch()) {
|
||||
$erreur = $translations['err_email_deja_utilise'];
|
||||
} else {
|
||||
// Mise à jour
|
||||
if ($mdp1) {
|
||||
$hash = password_hash($mdp1, PASSWORD_DEFAULT);
|
||||
$stmt = $pdo->prepare("UPDATE compte SET email = ?, mot_de_passe = ? WHERE id = ?");
|
||||
@@ -40,37 +44,48 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$stmt = $pdo->prepare("UPDATE compte SET email = ? WHERE id = ?");
|
||||
$stmt->execute([$nouvel_email, $id]);
|
||||
}
|
||||
$success = $translations['success_compte_mis_a_jour'];
|
||||
// Optionnel : mettre à jour $email_actuel pour refléter l'écran
|
||||
|
||||
$success = $translations['success_compte_mis_a_jour'];
|
||||
$email_actuel = $nouvel_email;
|
||||
|
||||
// garder la valeur en mémoire pour la suite de la page
|
||||
$compte['mot_de_passe'] = $mdp1 ? $hash : $compte['mot_de_passe'];
|
||||
}
|
||||
}
|
||||
}
|
||||
require_once '../includes/header.php';
|
||||
}
|
||||
require_once '../includes/header.php';
|
||||
?>
|
||||
|
||||
<div class="main1">
|
||||
<div class="container py-5">
|
||||
<form method="POST" class="shadow p-4 rounded bg-light">
|
||||
<h2 class="text-center mb-4"><?= htmlspecialchars($translations['h2_infos_connexion']) ?></h2>
|
||||
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(ensure_csrf_token(), ENT_QUOTES, 'UTF-8') ?>">
|
||||
<h2 class="text-center mb-4"><?php echo htmlspecialchars($translations['h2_infos_connexion']) ?></h2>
|
||||
|
||||
<?php if (!empty($erreur)): ?>
|
||||
<div class="alert alert-danger"><?= htmlspecialchars($erreur) ?></div>
|
||||
<?php if (! empty($erreur)): ?>
|
||||
<div class="alert alert-danger"><?php echo htmlspecialchars($erreur) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($success)): ?>
|
||||
<div class="alert alert-success"><?= htmlspecialchars($success) ?></div>
|
||||
<?php if (! empty($success)): ?>
|
||||
<div class="alert alert-success"><?php echo htmlspecialchars($success) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= htmlspecialchars($translations['label_email']) ?></label>
|
||||
<input type="email" name="email" class="form-control" value="<?= htmlspecialchars($email_actuel ?? '') ?>" required>
|
||||
<label class="form-label"><?php echo htmlspecialchars($translations['label_email']) ?></label>
|
||||
<input type="email" name="email" class="form-control" value="<?php echo htmlspecialchars($email_actuel ?? '') ?>" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?php echo htmlspecialchars($translations['label_mdp_actuel'] ?? 'Mot de passe actuel') ?></label>
|
||||
<div class="field-wrapper">
|
||||
<input type="password" name="mot_de_passe_actuel" id="mot_de_passe_actuel" class="form-control"
|
||||
placeholder="<?php echo htmlspecialchars($translations['placeholder_mdp_actuel'] ?? 'Entrez votre mot de passe actuel') ?>" required>
|
||||
<span class="toggle-password" onclick="toggle('mot_de_passe_actuel')">👁️</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= htmlspecialchars($translations['label_nouveau_mdp_opt']) ?></label>
|
||||
<label class="form-label"><?php echo htmlspecialchars($translations['label_nouveau_mdp_opt']) ?></label>
|
||||
<div class="field-wrapper">
|
||||
<input type="password" name="mot_de_passe" id="mot_de_passe" class="form-control"
|
||||
placeholder="<?= htmlspecialchars($translations['placeholder_nouveau_mdp']) ?>">
|
||||
placeholder="<?php echo htmlspecialchars($translations['placeholder_nouveau_mdp']) ?>">
|
||||
<span class="toggle-password" onclick="toggle('mot_de_passe')">👁️</span>
|
||||
</div>
|
||||
<div class="strength-bar mt-2">
|
||||
@@ -79,21 +94,21 @@ require_once '../includes/header.php';
|
||||
<div class="strength-fill"></div>
|
||||
</div>
|
||||
<div id="niveau-securite" class="mb-2 text-muted">
|
||||
<?= htmlspecialchars($translations['securite_label']) ?> : <?= htmlspecialchars($translations['securite_vide']) ?>
|
||||
<?php echo htmlspecialchars($translations['securite_label']) ?> : <?php echo htmlspecialchars($translations['securite_vide']) ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= htmlspecialchars($translations['label_confirmer_mdp']) ?></label>
|
||||
<label class="form-label"><?php echo htmlspecialchars($translations['label_confirmer_mdp']) ?></label>
|
||||
<div class="field-wrapper">
|
||||
<input type="password" name="confirm_mot_de_passe" id="confirm_mot_de_passe" class="form-control"
|
||||
placeholder="<?= htmlspecialchars($translations['placeholder_confirmer_mdp']) ?>">
|
||||
placeholder="<?php echo htmlspecialchars($translations['placeholder_confirmer_mdp']) ?>">
|
||||
<span class="toggle-password" onclick="toggle('confirm_mot_de_passe')">👁️</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 text-center">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<?= htmlspecialchars($translations['btn_mettre_a_jour']) ?>
|
||||
<?php echo htmlspecialchars($translations['btn_mettre_a_jour']) ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -112,11 +127,11 @@ document.getElementById('mot_de_passe').addEventListener('input', function () {
|
||||
segments.forEach(s => s.style.backgroundColor = '#ccc');
|
||||
|
||||
// base label
|
||||
const label = <?= json_encode($translations['securite_label'], JSON_UNESCAPED_UNICODE) ?>;
|
||||
const faible = <?= json_encode($translations['securite_faible'], JSON_UNESCAPED_UNICODE) ?>;
|
||||
const moyen = <?= json_encode($translations['securite_moyen'], JSON_UNESCAPED_UNICODE) ?>;
|
||||
const fort = <?= json_encode($translations['securite_fort'], JSON_UNESCAPED_UNICODE) ?>;
|
||||
const vide = <?= json_encode($translations['securite_vide'], JSON_UNESCAPED_UNICODE) ?>;
|
||||
const label = <?php echo json_encode($translations['securite_label'], JSON_UNESCAPED_UNICODE) ?>;
|
||||
const faible = <?php echo json_encode($translations['securite_faible'], JSON_UNESCAPED_UNICODE) ?>;
|
||||
const moyen = <?php echo json_encode($translations['securite_moyen'], JSON_UNESCAPED_UNICODE) ?>;
|
||||
const fort = <?php echo json_encode($translations['securite_fort'], JSON_UNESCAPED_UNICODE) ?>;
|
||||
const vide = <?php echo json_encode($translations['securite_vide'], JSON_UNESCAPED_UNICODE) ?>;
|
||||
|
||||
niveauText.textContent = `${label} : ${vide}`;
|
||||
niveauText.classList.remove("text-danger", "text-warning", "text-success");
|
||||
@@ -152,7 +167,7 @@ document.getElementById('mot_de_passe').addEventListener('input', function () {
|
||||
</div>
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p><?= $translations['copyr'] ?></p>
|
||||
<p><?php echo $translations['copyr'] ?></p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
|
||||
+208
-150
@@ -1,23 +1,25 @@
|
||||
<?php
|
||||
// /public/praticien/disponibilites.php
|
||||
declare (strict_types = 1);
|
||||
|
||||
require_once __DIR__ . '/../includes/init_praticien.php';
|
||||
|
||||
$praticien_id = (int) ($_SESSION['reference_id'] ?? 0);
|
||||
if ($praticien_id <= 0) {http_response_code(403);exit('Forbidden');}
|
||||
if ($praticien_id <= 0) {
|
||||
http_response_code(403);
|
||||
exit('Forbidden');
|
||||
}
|
||||
|
||||
// Durée RDV praticien (garde-fou strict)
|
||||
// Durée RDV praticien
|
||||
$stmt = $pdo->prepare("SELECT duree_rdv FROM praticien WHERE id = ?");
|
||||
$stmt->execute([$praticien_id]);
|
||||
$duree_minutes = (int) ($stmt->fetchColumn() ?: 0);
|
||||
if ($duree_minutes <= 0) {
|
||||
$duree_minutes = 30;
|
||||
}
|
||||
// fallback sécurité
|
||||
// ---- Ajouter un créneau (POST) ----
|
||||
|
||||
$erreur = null;
|
||||
|
||||
$toMin = function (string $hhmm): ?int {
|
||||
// input type="time" renvoie souvent HH:MM
|
||||
if (! preg_match('/^\d{2}:\d{2}$/', $hhmm)) {
|
||||
return null;
|
||||
}
|
||||
@@ -30,7 +32,14 @@
|
||||
return $h * 60 + $m;
|
||||
};
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['jour'], $_POST['heure_debut'], $_POST['heure_fin'])) {
|
||||
// ---- Ajouter un créneau (POST) ----
|
||||
if (
|
||||
$_SERVER['REQUEST_METHOD'] === 'POST'
|
||||
&& ($_POST['action'] ?? '') === 'add'
|
||||
&& isset($_POST['jour'], $_POST['heure_debut'], $_POST['heure_fin'])
|
||||
) {
|
||||
require_post_csrf();
|
||||
|
||||
$jour = trim((string) $_POST['jour']);
|
||||
$debut = trim((string) $_POST['heure_debut']);
|
||||
$fin = trim((string) $_POST['heure_fin']);
|
||||
@@ -43,7 +52,7 @@
|
||||
} elseif ($md >= $mf) {
|
||||
$erreur = $translations['erreur_debut_avant_fin'] ?? "Heure de début après l’heure de fin.";
|
||||
} else {
|
||||
$window = $mf - $md; // minutes
|
||||
$window = $mf - $md;
|
||||
|
||||
if ($window < $duree_minutes) {
|
||||
$erreur = sprintf(
|
||||
@@ -69,194 +78,238 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Supprimer un créneau (GET) ----
|
||||
// (idéalement: POST + CSRF, on garde GET pour aller vite)
|
||||
if (isset($_GET['supprimer'])) {
|
||||
$id = (int) $_GET['supprimer'];
|
||||
$stmt = $pdo->prepare("DELETE FROM disponibilite WHERE id = ? AND praticien_id = ?");
|
||||
$stmt->execute([$id, $praticien_id]);
|
||||
header('Location: /public/praticien/disponibilites.php?ok=deleted', true, 303);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ---- Récupérer les créneaux ----
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT * FROM disponibilite
|
||||
"SELECT *
|
||||
FROM disponibilite
|
||||
WHERE praticien_id = ?
|
||||
ORDER BY FIELD(jour_semaine,'lundi','mardi','mercredi','jeudi','vendredi','samedi','dimanche'), heure_debut"
|
||||
);
|
||||
$stmt->execute([$praticien_id]);
|
||||
$dispos = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Prépare les traductions de jours
|
||||
// Traductions jours
|
||||
$jourKeyMap = [
|
||||
'lundi' => 'jour_lundi', 'mardi' => 'jour_mardi', 'mercredi' => 'jour_mercredi',
|
||||
'jeudi' => 'jour_jeudi', 'vendredi' => 'jour_vendredi', 'samedi' => 'jour_samedi', 'dimanche' => 'jour_dimanche',
|
||||
'lundi' => 'jour_lundi',
|
||||
'mardi' => 'jour_mardi',
|
||||
'mercredi' => 'jour_mercredi',
|
||||
'jeudi' => 'jour_jeudi',
|
||||
'vendredi' => 'jour_vendredi',
|
||||
'samedi' => 'jour_samedi',
|
||||
'dimanche' => 'jour_dimanche',
|
||||
];
|
||||
// 1️⃣ Titre de la page
|
||||
$PAGE_TITLE = $translations['titre_disponibilites'] ?? 'Disponibilités';
|
||||
|
||||
// 2️⃣ Ce qui doit aller dans <head> EN PLUS du header global
|
||||
$PAGE_TITLE = $translations['titre_disponibilites'] ?? 'Disponibilités';
|
||||
$HEAD_EXTRA = <<<HTML
|
||||
<script src="/js/sweetalert2@11.js"></script>
|
||||
HTML;
|
||||
|
||||
require_once __DIR__ . '/../includes/header.php';
|
||||
?>
|
||||
|
||||
|
||||
<div class="main1">
|
||||
<div class="container py-5">
|
||||
<div class="shadow p-4 rounded bg-light">
|
||||
<h2 class="mb-4 text-center"><?php echo htmlspecialchars($translations['h2_disponibilites'] ?? 'Mes disponibilités') ?></h2>
|
||||
<div class="container py-5">
|
||||
<div class="shadow p-4 rounded bg-light">
|
||||
<h2 class="mb-4 text-center"><?php echo htmlspecialchars($translations['h2_disponibilites'] ?? 'Mes disponibilités'); ?></h2>
|
||||
|
||||
<!-- Plus d'alertes Bootstrap ici : on affiche via Swal selon ?ok=... et $erreur -->
|
||||
<div class="text-end">
|
||||
<a href="/public/praticien/disponibilites_date.php" class="btn btn-outline-secondary mb-4">
|
||||
<?php echo htmlspecialchars($translations['btn_ajouter_par_date'] ?? 'Ajouter par date') ?>
|
||||
</a>
|
||||
</div>
|
||||
<div class="table-responsive mb-4">
|
||||
<div class="text-end">
|
||||
<a href="/public/praticien/disponibilites_date.php" class="btn btn-outline-secondary mb-4">
|
||||
<?php echo htmlspecialchars($translations['btn_ajouter_par_date'] ?? 'Ajouter par date'); ?>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<table class="table table-bordered table-striped align-middle text-center mobile-cards" id="table_rdv">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th><?php echo htmlspecialchars($translations['table_jour'] ?? 'Jour') ?></th>
|
||||
<th><?php echo htmlspecialchars($translations['table_debut'] ?? 'Début') ?></th>
|
||||
<th><?php echo htmlspecialchars($translations['table_fin'] ?? 'Fin') ?></th>
|
||||
<th><?php echo htmlspecialchars($translations['table_action'] ?? 'Action') ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if ($dispos): foreach ($dispos as $d): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<?php
|
||||
$key = $jourKeyMap[$d['jour_semaine']] ?? null;
|
||||
echo htmlspecialchars($key ? ($translations[$key] ?? ucfirst($d['jour_semaine'])) : ucfirst($d['jour_semaine']));
|
||||
?>
|
||||
</td>
|
||||
<td><?php echo htmlspecialchars(substr((string) $d['heure_debut'], 0, 5)) ?></td>
|
||||
<td><?php echo htmlspecialchars(substr((string) $d['heure_fin'], 0, 5)) ?></td>
|
||||
<td>
|
||||
<button type="button"
|
||||
<div class="table-responsive mb-4">
|
||||
<table class="table table-bordered table-striped align-middle text-center mobile-cards" id="table_rdv">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th><?php echo htmlspecialchars($translations['table_jour'] ?? 'Jour'); ?></th>
|
||||
<th><?php echo htmlspecialchars($translations['table_debut'] ?? 'Début'); ?></th>
|
||||
<th><?php echo htmlspecialchars($translations['table_fin'] ?? 'Fin'); ?></th>
|
||||
<th><?php echo htmlspecialchars($translations['table_action'] ?? 'Action'); ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if ($dispos): ?>
|
||||
<?php foreach ($dispos as $d): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<?php
|
||||
$key = $jourKeyMap[$d['jour_semaine']] ?? null;
|
||||
echo htmlspecialchars($key ? ($translations[$key] ?? ucfirst($d['jour_semaine'])) : ucfirst($d['jour_semaine']));
|
||||
?>
|
||||
</td>
|
||||
<td><?php echo htmlspecialchars(substr((string) $d['heure_debut'], 0, 5)); ?></td>
|
||||
<td><?php echo htmlspecialchars(substr((string) $d['heure_fin'], 0, 5)); ?></td>
|
||||
<td>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-danger js-del"
|
||||
data-id="<?php echo (int) $d['id'] ?>">
|
||||
<?php echo htmlspecialchars($translations['btn_supprimer'] ?? 'Supprimer') ?>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach;else: ?>
|
||||
<tr><td colspan="4"><?php echo htmlspecialchars($translations['aucun_creneau'] ?? 'Aucun créneau.') ?></td></tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
data-id="<?php echo (int) $d['id']; ?>">
|
||||
<?php echo htmlspecialchars($translations['btn_supprimer'] ?? 'Supprimer'); ?>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr>
|
||||
<td colspan="4"><?php echo htmlspecialchars($translations['aucun_creneau'] ?? 'Aucun créneau.'); ?></td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm p-4">
|
||||
<h4 class="mb-3 text-center"><?php echo htmlspecialchars($translations['card_ajouter_creneau'] ?? 'Ajouter un créneau') ?></h4>
|
||||
<form method="POST">
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="jour"><?php echo htmlspecialchars($translations['label_jour'] ?? 'Jour') ?></label>
|
||||
<select name="jour" id="jour" class="form-select" required>
|
||||
<option value=""><?php echo htmlspecialchars($translations['option_choisir'] ?? '— Choisir —') ?></option>
|
||||
<?php
|
||||
$jours = [
|
||||
'lundi' => $translations['jour_lundi'] ?? 'Lundi',
|
||||
'mardi' => $translations['jour_mardi'] ?? 'Mardi',
|
||||
'mercredi' => $translations['jour_mercredi'] ?? 'Mercredi',
|
||||
'jeudi' => $translations['jour_jeudi'] ?? 'Jeudi',
|
||||
'vendredi' => $translations['jour_vendredi'] ?? 'Vendredi',
|
||||
'samedi' => $translations['jour_samedi'] ?? 'Samedi',
|
||||
'dimanche' => $translations['jour_dimanche'] ?? 'Dimanche',
|
||||
];
|
||||
foreach ($jours as $val => $label):
|
||||
?>
|
||||
<option value="<?php echo htmlspecialchars($val) ?>"><?php echo htmlspecialchars($label) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="card shadow-sm p-4">
|
||||
<h4 class="mb-3 text-center"><?php echo htmlspecialchars($translations['card_ajouter_creneau'] ?? 'Ajouter un créneau'); ?></h4>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="heure_debut"><?php echo htmlspecialchars($translations['label_heure_debut'] ?? 'Heure de début') ?></label>
|
||||
<input type="time" name="heure_debut" id="heure_debut" class="form-control" required>
|
||||
</div>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="action" value="add">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(ensure_csrf_token(), ENT_QUOTES, 'UTF-8'); ?>">
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="heure_fin"><?php echo htmlspecialchars($translations['label_heure_fin'] ?? 'Heure de fin') ?></label>
|
||||
<input type="time" name="heure_fin" id="heure_fin" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="jour"><?php echo htmlspecialchars($translations['label_jour'] ?? 'Jour'); ?></label>
|
||||
<select name="jour" id="jour" class="form-select" required>
|
||||
<option value=""><?php echo htmlspecialchars($translations['option_choisir'] ?? '— Choisir —'); ?></option>
|
||||
<?php
|
||||
$jours = [
|
||||
'lundi' => $translations['jour_lundi'] ?? 'Lundi',
|
||||
'mardi' => $translations['jour_mardi'] ?? 'Mardi',
|
||||
'mercredi' => $translations['jour_mercredi'] ?? 'Mercredi',
|
||||
'jeudi' => $translations['jour_jeudi'] ?? 'Jeudi',
|
||||
'vendredi' => $translations['jour_vendredi'] ?? 'Vendredi',
|
||||
'samedi' => $translations['jour_samedi'] ?? 'Samedi',
|
||||
'dimanche' => $translations['jour_dimanche'] ?? 'Dimanche',
|
||||
];
|
||||
foreach ($jours as $val => $label):
|
||||
?>
|
||||
<option value="<?php echo htmlspecialchars($val); ?>"><?php echo htmlspecialchars($label); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
<?php echo htmlspecialchars($translations['btn_ajouter'] ?? 'Ajouter') ?>
|
||||
</button>
|
||||
</form>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="heure_debut"><?php echo htmlspecialchars($translations['label_heure_debut'] ?? 'Heure de début'); ?></label>
|
||||
<input type="time" name="heure_debut" id="heure_debut" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="heure_fin"><?php echo htmlspecialchars($translations['label_heure_fin'] ?? 'Heure de fin'); ?></label>
|
||||
<input type="time" name="heure_fin" id="heure_fin" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
<?php echo htmlspecialchars($translations['btn_ajouter'] ?? 'Ajouter'); ?>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// --- SweetAlert2: messages succès / erreur depuis PHP ---
|
||||
document.addEventListener('DOMContentLoaded', function(){
|
||||
const okParam = '<?php echo isset($_GET['ok']) ? addslashes((string) $_GET['ok']) : '' ?>';
|
||||
const erreurMsg = '<?php echo $erreur ? addslashes((string) $erreur) : '' ?>';
|
||||
const csrfToken = <?php echo json_encode(ensure_csrf_token(), JSON_UNESCAPED_UNICODE); ?>;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const okParam = <?php echo json_encode(isset($_GET['ok']) ? (string) $_GET['ok'] : '', JSON_UNESCAPED_UNICODE); ?>;
|
||||
const erreurMsg = <?php echo json_encode($erreur ?: '', JSON_UNESCAPED_UNICODE); ?>;
|
||||
|
||||
if (okParam === 'added') {
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: '<?php echo addslashes($translations["msg_creneau_ajoute"] ?? "Créneau ajouté.") ?>',
|
||||
timer: 1800,
|
||||
showConfirmButton: false
|
||||
});
|
||||
} else if (okParam === 'deleted') {
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: '<?php echo addslashes($translations["msg_creneau_supprime"] ?? "Créneau supprimé.") ?>',
|
||||
timer: 1800,
|
||||
showConfirmButton: false
|
||||
});
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: <?php echo json_encode($translations['msg_creneau_ajoute'] ?? 'Créneau ajouté.', JSON_UNESCAPED_UNICODE); ?>,
|
||||
timer: 1800,
|
||||
showConfirmButton: false
|
||||
});
|
||||
|
||||
if (window.history.replaceState) {
|
||||
const cleanUrl = window.location.pathname;
|
||||
window.history.replaceState({}, document.title, cleanUrl);
|
||||
}
|
||||
}
|
||||
|
||||
if (erreurMsg) {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: '<?php echo addslashes($translations["erreur_titre"] ?? "Erreur") ?>',
|
||||
html: erreurMsg,
|
||||
showCancelButton: true,
|
||||
confirmButtonText: '<?php echo addslashes($translations["btn_modifier_profil"] ?? "Modifier mon profil") ?>',
|
||||
cancelButtonText: 'OK'
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
window.location.href = '/public/praticien/modifier.php';
|
||||
}
|
||||
});
|
||||
}
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: <?php echo json_encode($translations['erreur_titre'] ?? 'Erreur', JSON_UNESCAPED_UNICODE); ?>,
|
||||
html: erreurMsg,
|
||||
showCancelButton: true,
|
||||
confirmButtonText: <?php echo json_encode($translations['btn_modifier_profil'] ?? 'Modifier mon profil', JSON_UNESCAPED_UNICODE); ?>,
|
||||
cancelButtonText: 'OK'
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
window.location.href = '/public/praticien/modifier.php';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// --- Confirmation de suppression avec SweetAlert2 ---
|
||||
document.addEventListener('click', async function(e){
|
||||
document.addEventListener('click', async function (e) {
|
||||
const btn = e.target.closest('.js-del');
|
||||
if (!btn) return;
|
||||
|
||||
const id = btn.dataset.id;
|
||||
if (!id) return;
|
||||
|
||||
const result = await Swal.fire({
|
||||
icon: 'warning',
|
||||
title: '<?php echo addslashes($translations["confirm_title"] ?? "Confirmation") ?>',
|
||||
text: '<?php echo addslashes($translations["confirm_supprimer_creneau"] ?? "Supprimer ce créneau ?") ?>',
|
||||
title: <?php echo json_encode($translations['confirm_title'] ?? 'Confirmation', JSON_UNESCAPED_UNICODE); ?>,
|
||||
text: <?php echo json_encode($translations['confirm_supprimer_creneau'] ?? 'Supprimer ce créneau ?', JSON_UNESCAPED_UNICODE); ?>,
|
||||
showCancelButton: true,
|
||||
confirmButtonText: '<?php echo addslashes($translations["btn_da_udalit"] ?? "Oui") ?>',
|
||||
cancelButtonText: '<?php echo addslashes($translations["btn_annuler"] ?? "Annuler") ?>',
|
||||
confirmButtonText: <?php echo json_encode($translations['btn_da_udalit'] ?? 'Oui', JSON_UNESCAPED_UNICODE); ?>,
|
||||
cancelButtonText: <?php echo json_encode($translations['btn_annuler'] ?? 'Annuler', JSON_UNESCAPED_UNICODE); ?>,
|
||||
reverseButtons: true
|
||||
});
|
||||
|
||||
if (result.isConfirmed) {
|
||||
// Suppression côté serveur (GET) — garde la mécanique existante
|
||||
window.location.href = '?supprimer=' + encodeURIComponent(id);
|
||||
}
|
||||
if (!result.isConfirmed) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('id', String(id));
|
||||
formData.append('csrf_token', csrfToken);
|
||||
|
||||
fetch('/public/praticien/supprimer_disponibilite.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: formData
|
||||
})
|
||||
.then(async r => {
|
||||
const text = await r.text();
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch (e) {
|
||||
return { ok: false, message: text };
|
||||
}
|
||||
})
|
||||
.then(data => {
|
||||
if (data.ok) {
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: <?php echo json_encode($translations['msg_creneau_supprime'] ?? 'Créneau supprimé.', JSON_UNESCAPED_UNICODE); ?>,
|
||||
timer: 1400,
|
||||
showConfirmButton: false
|
||||
}).then(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
} else {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: <?php echo json_encode($translations['erreur_titre'] ?? 'Erreur', JSON_UNESCAPED_UNICODE); ?>,
|
||||
text: data.message || 'Suppression impossible.'
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'Erreur réseau',
|
||||
text: 'Veuillez réessayer.'
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<!--*********Adaptation des tables pour un affichage mobile***************-->
|
||||
<script defer>
|
||||
(function(){
|
||||
function initMobileCards(tbl){
|
||||
@@ -269,15 +322,20 @@ document.addEventListener('click', async function(e){
|
||||
});
|
||||
document.documentElement.classList.add('mc-ready');
|
||||
}
|
||||
function ready(fn){ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fn); else fn(); }
|
||||
ready(function(){ initMobileCards(document.getElementById('table_rdv')); });
|
||||
function ready(fn){
|
||||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fn);
|
||||
else fn();
|
||||
}
|
||||
ready(function(){
|
||||
initMobileCards(document.getElementById('table_rdv'));
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p><?php echo $translations['copyr'] ?? '© ' . date('Y') ?></p>
|
||||
<p><?php echo $translations['copyr'] ?? '© ' . date('Y'); ?></p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
@@ -1,17 +1,23 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
|
||||
// /public/praticien/disponibilites_date.php
|
||||
require_once __DIR__ . '/../includes/init_praticien.php';
|
||||
$praticien_id = (int) ($_SESSION['reference_id'] ?? 0);
|
||||
if ($praticien_id <= 0) {http_response_code(403);exit('Forbidden');}
|
||||
|
||||
// Durée RDV praticien (garde-fou strict)
|
||||
$praticien_id = (int) ($_SESSION['reference_id'] ?? 0);
|
||||
if ($praticien_id <= 0) {
|
||||
http_response_code(403);
|
||||
exit('Forbidden');
|
||||
}
|
||||
|
||||
// Durée RDV praticien
|
||||
$stmt = $pdo->prepare("SELECT duree_rdv FROM praticien WHERE id = ?");
|
||||
$stmt->execute([$praticien_id]);
|
||||
$duree_minutes = (int) ($stmt->fetchColumn() ?: 0);
|
||||
if ($duree_minutes <= 0) {
|
||||
$duree_minutes = 30;
|
||||
}
|
||||
// ---- Ajouter une dispo ponctuelle (POST) ----
|
||||
|
||||
$erreur = null;
|
||||
|
||||
$toMin = function (string $hhmm): ?int {
|
||||
@@ -27,13 +33,20 @@
|
||||
return $h * 60 + $m;
|
||||
};
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['date_dispo'], $_POST['heure_debut'], $_POST['heure_fin'])) {
|
||||
// ---- Ajouter une dispo ponctuelle (POST + CSRF)
|
||||
if (
|
||||
$_SERVER['REQUEST_METHOD'] === 'POST'
|
||||
&& ($_POST['action'] ?? '') === 'add'
|
||||
&& isset($_POST['date_dispo'], $_POST['heure_debut'], $_POST['heure_fin'])
|
||||
) {
|
||||
require_post_csrf();
|
||||
|
||||
$date_dispo = trim((string) $_POST['date_dispo']); // YYYY-MM-DD
|
||||
$debut = trim((string) $_POST['heure_debut']); // HH:MM
|
||||
$fin = trim((string) $_POST['heure_fin']); // HH:MM
|
||||
|
||||
// Validation date simple
|
||||
$dt = DateTimeImmutable::createFromFormat('Y-m-d', $date_dispo);
|
||||
|
||||
if (! $dt || $dt->format('Y-m-d') !== $date_dispo) {
|
||||
$erreur = $translations['erreur_format_date'] ?? "Format de date invalide.";
|
||||
} else {
|
||||
@@ -59,10 +72,10 @@
|
||||
$duree_minutes
|
||||
);
|
||||
} else {
|
||||
$stmt = $pdo->prepare(
|
||||
"INSERT INTO disponibilite_date (praticien_id, date_dispo, heure_debut, heure_fin)
|
||||
VALUES (?, ?, ?, ?)"
|
||||
);
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO disponibilite_date (praticien_id, date_dispo, heure_debut, heure_fin)
|
||||
VALUES (?, ?, ?, ?)
|
||||
");
|
||||
$stmt->execute([$praticien_id, $date_dispo, $debut, $fin]);
|
||||
|
||||
header('Location: /public/praticien/disponibilites_date.php?ok=added', true, 303);
|
||||
@@ -72,158 +85,209 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Supprimer une plage datée (GET) ----
|
||||
if (isset($_GET['supprimer'])) {
|
||||
$id = (int) $_GET['supprimer'];
|
||||
$stmt = $pdo->prepare("DELETE FROM disponibilite_date WHERE id = ? AND praticien_id = ?");
|
||||
$stmt->execute([$id, $praticien_id]);
|
||||
header('Location: /public/praticien/disponibilites_date.php?ok=deleted', true, 303);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ---- Récupérer les plages datées ----
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT id, date_dispo, heure_debut, heure_fin
|
||||
FROM disponibilite_date
|
||||
WHERE praticien_id = ?
|
||||
ORDER BY date_dispo ASC, heure_debut ASC"
|
||||
);
|
||||
// ---- Récupérer les plages datées
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT id, date_dispo, heure_debut, heure_fin
|
||||
FROM disponibilite_date
|
||||
WHERE praticien_id = ?
|
||||
ORDER BY date_dispo ASC, heure_debut ASC
|
||||
");
|
||||
$stmt->execute([$praticien_id]);
|
||||
$dates = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
// 1️⃣ Titre de la page
|
||||
$PAGE_TITLE = $translations['titre_disponibilites_date'] ?? 'Disponibilités (dates)';
|
||||
|
||||
// 2️⃣ Ce qui doit aller dans <head> EN PLUS du header global
|
||||
$PAGE_TITLE = $translations['titre_disponibilites_date'] ?? 'Disponibilités (dates)';
|
||||
$HEAD_EXTRA = <<<HTML
|
||||
<script src="/js/sweetalert2@11.js"></script>
|
||||
HTML;
|
||||
|
||||
require_once __DIR__ . '/../includes/header.php';
|
||||
?>
|
||||
|
||||
|
||||
<div class="main1">
|
||||
<div class="container py-5">
|
||||
<div class="shadow p-4 rounded bg-light">
|
||||
<h2 class="mb-4 text-center"><?php echo htmlspecialchars($translations['h2_disponibilites_date'] ?? 'Mes disponibilités ponctuelles') ?></h2>
|
||||
<div class="container py-5">
|
||||
<div class="shadow p-4 rounded bg-light">
|
||||
<h2 class="mb-4 text-center">
|
||||
<?php echo htmlspecialchars($translations['h2_disponibilites_date'] ?? 'Mes disponibilités ponctuelles'); ?>
|
||||
</h2>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<a class="btn btn-outline-secondary"
|
||||
href="/public/praticien/disponibilites.php">
|
||||
<?php echo htmlspecialchars($translations['retour_au_planning'] ?? 'Retour') ?>
|
||||
</a>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<a class="btn btn-outline-secondary" href="/public/praticien/disponibilites.php">
|
||||
<?php echo htmlspecialchars($translations['retour_au_planning'] ?? 'Retour'); ?>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive mb-4">
|
||||
<table class="table table-bordered table-striped align-middle text-center mobile-cards" id="table_rdv">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th><?php echo htmlspecialchars($translations['cl_date_rdv'] ?? 'Date') ?></th>
|
||||
<th><?php echo htmlspecialchars($translations['table_debut'] ?? 'Début') ?></th>
|
||||
<th><?php echo htmlspecialchars($translations['table_fin'] ?? 'Fin') ?></th>
|
||||
<th><?php echo htmlspecialchars($translations['table_action'] ?? 'Action') ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if ($dates): foreach ($dates as $d): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<?php echo htmlspecialchars(
|
||||
date('d/m/Y', strtotime($d['date_dispo']))
|
||||
) ?>
|
||||
</td>
|
||||
<td><?php echo htmlspecialchars(substr((string) $d['heure_debut'], 0, 5)) ?></td>
|
||||
<td><?php echo htmlspecialchars(substr((string) $d['heure_fin'], 0, 5)) ?></td>
|
||||
<td>
|
||||
<button type="button"
|
||||
<div class="table-responsive mb-4">
|
||||
<table class="table table-bordered table-striped align-middle text-center mobile-cards" id="table_rdv">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th><?php echo htmlspecialchars($translations['cl_date_rdv'] ?? 'Date'); ?></th>
|
||||
<th><?php echo htmlspecialchars($translations['table_debut'] ?? 'Début'); ?></th>
|
||||
<th><?php echo htmlspecialchars($translations['table_fin'] ?? 'Fin'); ?></th>
|
||||
<th><?php echo htmlspecialchars($translations['table_action'] ?? 'Action'); ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if ($dates): ?>
|
||||
<?php foreach ($dates as $d): ?>
|
||||
<tr>
|
||||
<td><?php echo htmlspecialchars(date('d/m/Y', strtotime((string) $d['date_dispo']))); ?></td>
|
||||
<td><?php echo htmlspecialchars(substr((string) $d['heure_debut'], 0, 5)); ?></td>
|
||||
<td><?php echo htmlspecialchars(substr((string) $d['heure_fin'], 0, 5)); ?></td>
|
||||
<td>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-danger js-del"
|
||||
data-id="<?php echo (int) $d['id'] ?>">
|
||||
<?php echo htmlspecialchars($translations['btn_supprimer'] ?? 'Supprimer') ?>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach;else: ?>
|
||||
<tr><td colspan="4"><?php echo htmlspecialchars($translations['aucun_creneau'] ?? 'Aucun créneau.') ?></td></tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
data-id="<?php echo (int) $d['id']; ?>">
|
||||
<?php echo htmlspecialchars($translations['btn_supprimer'] ?? 'Supprimer'); ?>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr>
|
||||
<td colspan="4"><?php echo htmlspecialchars($translations['aucun_creneau'] ?? 'Aucun créneau.'); ?></td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm p-4">
|
||||
<h4 class="mb-3 text-center"><?php echo htmlspecialchars($translations['card_ajouter_creneau_date'] ?? 'Ajouter une disponibilité ponctuelle') ?></h4>
|
||||
<div class="card shadow-sm p-4">
|
||||
<h4 class="mb-3 text-center">
|
||||
<?php echo htmlspecialchars($translations['card_ajouter_creneau_date'] ?? 'Ajouter une disponibilité ponctuelle'); ?>
|
||||
</h4>
|
||||
|
||||
<form method="POST">
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="date_dispo"><?php echo htmlspecialchars($translations['cl_date_rdv'] ?? 'Date') ?></label>
|
||||
<input type="date" name="date_dispo" id="date_dispo" class="form-control" required>
|
||||
</div>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="action" value="add">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(ensure_csrf_token(), ENT_QUOTES, 'UTF-8'); ?>">
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="heure_debut"><?php echo htmlspecialchars($translations['label_heure_debut'] ?? 'Heure de début') ?></label>
|
||||
<input type="time" name="heure_debut" id="heure_debut" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="date_dispo"><?php echo htmlspecialchars($translations['cl_date_rdv'] ?? 'Date'); ?></label>
|
||||
<input type="date" name="date_dispo" id="date_dispo" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="heure_fin"><?php echo htmlspecialchars($translations['label_heure_fin'] ?? 'Heure de fin') ?></label>
|
||||
<input type="time" name="heure_fin" id="heure_fin" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="heure_debut"><?php echo htmlspecialchars($translations['label_heure_debut'] ?? 'Heure de début'); ?></label>
|
||||
<input type="time" name="heure_debut" id="heure_debut" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
<?php echo htmlspecialchars($translations['btn_ajouter'] ?? 'Ajouter') ?>
|
||||
</button>
|
||||
</form>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="heure_fin"><?php echo htmlspecialchars($translations['label_heure_fin'] ?? 'Heure de fin'); ?></label>
|
||||
<input type="time" name="heure_fin" id="heure_fin" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
<?php echo htmlspecialchars($translations['btn_ajouter'] ?? 'Ajouter'); ?>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const csrfToken = <?php echo json_encode(ensure_csrf_token(), JSON_UNESCAPED_UNICODE); ?>;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function(){
|
||||
const okParam = '<?php echo isset($_GET['ok']) ? addslashes((string) $_GET['ok']) : '' ?>';
|
||||
const erreurMsg = '<?php echo $erreur ? addslashes((string) $erreur) : '' ?>';
|
||||
const okParam = <?php echo json_encode(isset($_GET['ok']) ? (string) $_GET['ok'] : '', JSON_UNESCAPED_UNICODE); ?>;
|
||||
const erreurMsg = <?php echo json_encode($erreur ?: '', JSON_UNESCAPED_UNICODE); ?>;
|
||||
|
||||
if (okParam === 'added') {
|
||||
Swal.fire({ icon:'success', title:'<?php echo addslashes($translations["msg_creneau_ajoute"] ?? "Créneau ajouté.") ?>', timer:1800, showConfirmButton:false });
|
||||
} else if (okParam === 'deleted') {
|
||||
Swal.fire({ icon:'success', title:'<?php echo addslashes($translations["msg_creneau_supprime"] ?? "Créneau supprimé.") ?>', timer:1800, showConfirmButton:false });
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: <?php echo json_encode($translations["msg_creneau_ajoute"] ?? "Créneau ajouté.", JSON_UNESCAPED_UNICODE); ?>,
|
||||
timer: 1800,
|
||||
showConfirmButton: false
|
||||
});
|
||||
|
||||
if (window.history.replaceState) {
|
||||
window.history.replaceState({}, document.title, window.location.pathname);
|
||||
}
|
||||
}
|
||||
|
||||
if (erreurMsg) {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: '<?php echo addslashes($translations["erreur_titre"] ?? "Erreur") ?>',
|
||||
html: erreurMsg,
|
||||
showCancelButton: true,
|
||||
confirmButtonText: '<?php echo addslashes($translations["btn_modifier_profil"] ?? "Modifier mon profil") ?>',
|
||||
cancelButtonText: 'OK'
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
window.location.href = '/public/praticien/modifier.php';
|
||||
}
|
||||
});
|
||||
}
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: <?php echo json_encode($translations["erreur_titre"] ?? "Erreur", JSON_UNESCAPED_UNICODE); ?>,
|
||||
html: erreurMsg,
|
||||
showCancelButton: true,
|
||||
confirmButtonText: <?php echo json_encode($translations["btn_modifier_profil"] ?? "Modifier mon profil", JSON_UNESCAPED_UNICODE); ?>,
|
||||
cancelButtonText: 'OK'
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
window.location.href = '/public/praticien/modifier.php';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', async function(e){
|
||||
const btn = e.target.closest('.js-del');
|
||||
if (!btn) return;
|
||||
|
||||
const id = btn.dataset.id;
|
||||
if (!id) return;
|
||||
|
||||
const result = await Swal.fire({
|
||||
icon: 'warning',
|
||||
title: '<?php echo addslashes($translations["confirm_title"] ?? "Confirmation") ?>',
|
||||
text: '<?php echo addslashes($translations["confirm_supprimer_creneau"] ?? "Supprimer ce créneau ?") ?>',
|
||||
title: <?php echo json_encode($translations["confirm_title"] ?? "Confirmation", JSON_UNESCAPED_UNICODE); ?>,
|
||||
text: <?php echo json_encode($translations["confirm_supprimer_creneau"] ?? "Supprimer ce créneau ?", JSON_UNESCAPED_UNICODE); ?>,
|
||||
showCancelButton: true,
|
||||
confirmButtonText: '<?php echo addslashes($translations["btn_da_udalit"] ?? "Oui") ?>',
|
||||
cancelButtonText: '<?php echo addslashes($translations["btn_annuler"] ?? "Annuler") ?>',
|
||||
confirmButtonText: <?php echo json_encode($translations["btn_da_udalit"] ?? "Oui", JSON_UNESCAPED_UNICODE); ?>,
|
||||
cancelButtonText: <?php echo json_encode($translations["btn_annuler"] ?? "Annuler", JSON_UNESCAPED_UNICODE); ?>,
|
||||
reverseButtons: true
|
||||
});
|
||||
|
||||
if (result.isConfirmed) {
|
||||
window.location.href = '?supprimer=' + encodeURIComponent(id);
|
||||
}
|
||||
if (!result.isConfirmed) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('id', String(id));
|
||||
formData.append('csrf_token', csrfToken);
|
||||
|
||||
fetch('/public/praticien/supprimer_disponibilite_date.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: formData
|
||||
})
|
||||
.then(async r => {
|
||||
const text = await r.text();
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch (e) {
|
||||
return { ok: false, message: text };
|
||||
}
|
||||
})
|
||||
.then(data => {
|
||||
if (data.ok) {
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: <?php echo json_encode($translations["msg_creneau_supprime"] ?? "Créneau supprimé.", JSON_UNESCAPED_UNICODE); ?>,
|
||||
timer: 1400,
|
||||
showConfirmButton: false
|
||||
}).then(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
} else {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: <?php echo json_encode($translations["erreur_titre"] ?? "Erreur", JSON_UNESCAPED_UNICODE); ?>,
|
||||
text: data.message || 'Suppression impossible.'
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'Erreur réseau',
|
||||
text: 'Veuillez réessayer.'
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<!--*********Adaptation des tables pour un affichage mobile***************-->
|
||||
|
||||
<script defer>
|
||||
(function(){
|
||||
function initMobileCards(tbl){
|
||||
@@ -236,14 +300,20 @@ document.addEventListener('click', async function(e){
|
||||
});
|
||||
document.documentElement.classList.add('mc-ready');
|
||||
}
|
||||
function ready(fn){ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fn); else fn(); }
|
||||
ready(function(){ initMobileCards(document.getElementById('table_rdv')); });
|
||||
function ready(fn){
|
||||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fn);
|
||||
else fn();
|
||||
}
|
||||
ready(function(){
|
||||
initMobileCards(document.getElementById('table_rdv'));
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p><?php echo $translations['copyr'] ?? '© ' . date('Y') ?></p>
|
||||
<p><?php echo $translations['copyr'] ?? '© ' . date('Y'); ?></p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
@@ -1,86 +1,113 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../includes/init_praticien.php';
|
||||
|
||||
require_once __DIR__ . '/../includes/init_praticien.php';
|
||||
$id = $_SESSION['reference_id'] ?? null;
|
||||
if (! $id) {
|
||||
http_response_code(403);
|
||||
exit('Forbidden');
|
||||
}
|
||||
|
||||
$currentLang = $lang ?? 'ru';
|
||||
if (! in_array($currentLang, ['ru', 'fr', 'en'], true)) {
|
||||
$currentLang = 'ru';
|
||||
}
|
||||
|
||||
// Base praticien
|
||||
$stmt = $pdo->prepare("SELECT * FROM praticien WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$praticien = $stmt->fetch();
|
||||
|
||||
// Récupérer les infos du praticien
|
||||
$stmt = $pdo->prepare("SELECT * FROM praticien WHERE id = ?");
|
||||
$stmt->execute([$_SESSION['reference_id'] ?? 0]);
|
||||
$praticien = $stmt->fetch();
|
||||
if (! $praticien) {
|
||||
http_response_code(404);
|
||||
exit(htmlspecialchars($translations['praticien_introuvable'] ?? 'Introuvable.'));
|
||||
}
|
||||
|
||||
$id = $_SESSION['reference_id'] ?? null;
|
||||
if (!$id) { http_response_code(403); exit('Forbidden'); }
|
||||
require_once __DIR__ . '/../includes/header.php';
|
||||
// Traduction courante
|
||||
$stmtTr = $pdo->prepare("
|
||||
SELECT *
|
||||
FROM praticien_traduction
|
||||
WHERE praticien_id = ? AND langue = ?
|
||||
LIMIT 1
|
||||
");
|
||||
$stmtTr->execute([$id, $currentLang]);
|
||||
$traduction = $stmtTr->fetch() ?: null;
|
||||
|
||||
// Valeurs affichées avec fallback
|
||||
$nomAffiche = $traduction['nom_affiche'] ?? $praticien['nom'] ?? '';
|
||||
$prenomAffiche = $traduction['prenom_affiche'] ?? $praticien['prenom'] ?? '';
|
||||
$villeAffichee = $traduction['ville_affichee'] ?? $praticien['ville'] ?? '';
|
||||
$titreAffiche = $traduction['titre'] ?? $praticien['titre'] ?? '';
|
||||
$specialiteAffiche = $traduction['specialite'] ?? $praticien['specialite'] ?? '';
|
||||
$presentationAffiche = $traduction['presentation'] ?? $praticien['presentation'] ?? '';
|
||||
$dispoAffiche = $traduction['disponibilite'] ?? $praticien['disponibilite'] ?? '';
|
||||
|
||||
require_once __DIR__ . '/../includes/header.php';
|
||||
?>
|
||||
|
||||
<div class="main1">
|
||||
<div class="container py-5">
|
||||
<div class="shadow p-4 rounded bg-light">
|
||||
<h2 class="mb-4 text-center"><?= htmlspecialchars($translations['mes_infos_heading']) ?></h2>
|
||||
<h2 class="mb-4 text-center"><?php echo htmlspecialchars($translations['mes_infos_heading']) ?></h2>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="row g-4 mb-3">
|
||||
<!-- Colonne gauche -->
|
||||
<div class="col-12 col-sm-6">
|
||||
<p><strong><?= htmlspecialchars($translations['label_nom']) ?> :</strong>
|
||||
<?= htmlspecialchars($praticien['nom'] ?? '') ?></p>
|
||||
<p><strong><?php echo htmlspecialchars($translations['label_nom']) ?> :</strong>
|
||||
<?php echo htmlspecialchars($nomAffiche) ?></p>
|
||||
|
||||
<p><strong><?= htmlspecialchars($translations['label_prenom']) ?> :</strong>
|
||||
<?= htmlspecialchars($praticien['prenom'] ?? '') ?></p>
|
||||
<p><strong><?php echo htmlspecialchars($translations['label_prenom']) ?> :</strong>
|
||||
<?php echo htmlspecialchars($prenomAffiche) ?></p>
|
||||
|
||||
<p><strong><?= htmlspecialchars($translations['label_email']) ?> :</strong>
|
||||
<?= htmlspecialchars($praticien['email'] ?? '') ?></p>
|
||||
<p><strong><?php echo htmlspecialchars($translations['label_email']) ?> :</strong>
|
||||
<?php echo htmlspecialchars($praticien['email'] ?? '') ?></p>
|
||||
|
||||
<p><strong><?= htmlspecialchars($translations['label_telephone']) ?> :</strong>
|
||||
<?= htmlspecialchars($praticien['telephone'] ?? '') ?></p>
|
||||
<p><strong><?php echo htmlspecialchars($translations['label_telephone']) ?> :</strong>
|
||||
<?php echo htmlspecialchars($praticien['telephone'] ?? '') ?></p>
|
||||
|
||||
<div>
|
||||
<strong><?= htmlspecialchars($translations['alt_photo']) ?> :</strong><br><br>
|
||||
<?php if (!empty($praticien['photo'])): ?>
|
||||
<strong><?php echo htmlspecialchars($translations['alt_photo']) ?> :</strong><br><br>
|
||||
<?php if (! empty($praticien['photo'])): ?>
|
||||
<img
|
||||
src="/public/uploads/<?= htmlspecialchars($praticien['photo']) ?>"
|
||||
alt="<?= htmlspecialchars($translations['alt_photo']) ?>"
|
||||
src="/public/uploads/<?php echo htmlspecialchars($praticien['photo']) ?>"
|
||||
alt="<?php echo htmlspecialchars($translations['alt_photo']) ?>"
|
||||
class="img-fluid rounded-4 shadow"
|
||||
style="max-width:230px;height:auto;">
|
||||
<?php else: ?>
|
||||
<span class="text-muted"><?= htmlspecialchars($translations['aucune_photo'] ?? 'Aucune photo') ?></span>
|
||||
<span class="text-muted"><?php echo htmlspecialchars($translations['aucune_photo'] ?? 'Aucune photo') ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Colonne droite -->
|
||||
<div class="col-12 col-sm-6">
|
||||
<p><strong><?= htmlspecialchars($translations['placeholder_siret']) ?> :</strong>
|
||||
<?= htmlspecialchars($praticien['siret'] ?? '') ?></p>
|
||||
<p><strong><?php echo htmlspecialchars($translations['placeholder_siret']) ?> :</strong>
|
||||
<?php echo htmlspecialchars($praticien['siret'] ?? '') ?></p>
|
||||
|
||||
<p><strong><?= htmlspecialchars($translations['label_specialite']) ?> :</strong>
|
||||
<?= htmlspecialchars($praticien['specialite'] ?? '') ?></p>
|
||||
<p><strong><?php echo htmlspecialchars($translations['label_specialite']) ?> :</strong>
|
||||
<?php echo htmlspecialchars($specialiteAffiche) ?></p>
|
||||
|
||||
<!-- Ajout tarif et moyen de paiement -->
|
||||
<p><strong><?= htmlspecialchars($translations['tarif_consultation']) ?> :</strong>
|
||||
<?= htmlspecialchars($praticien['titre'] ?? '') ?></p>
|
||||
<p><strong><?php echo htmlspecialchars($translations['tarif_consultation']) ?> :</strong>
|
||||
<?php echo htmlspecialchars($titreAffiche) ?></p>
|
||||
|
||||
<p><strong><?= htmlspecialchars($translations['moyens_payement']) ?> :</strong>
|
||||
<?= htmlspecialchars($praticien['disponibilite'] ?? '') ?></p>
|
||||
<p><strong><?php echo htmlspecialchars($translations['moyens_payement']) ?> :</strong>
|
||||
<?php echo htmlspecialchars($dispoAffiche) ?></p>
|
||||
|
||||
<p><strong><?= htmlspecialchars($translations['label_ville']) ?> :</strong>
|
||||
<?= htmlspecialchars($praticien['ville'] ?? '') ?></p>
|
||||
<p><strong><?php echo htmlspecialchars($translations['label_ville']) ?> :</strong>
|
||||
<?php echo htmlspecialchars($villeAffichee) ?></p>
|
||||
|
||||
<p><strong><?= htmlspecialchars($translations['label_code_postal']) ?> :</strong>
|
||||
<?= htmlspecialchars($praticien['code_postal'] ?? '') ?></p>
|
||||
<p><strong><?php echo htmlspecialchars($translations['label_code_postal']) ?> :</strong>
|
||||
<?php echo htmlspecialchars($praticien['code_postal'] ?? '') ?></p>
|
||||
|
||||
<p><strong><?= htmlspecialchars($translations['label_adresse']) ?> :</strong><br>
|
||||
<?= nl2br(htmlspecialchars($praticien['adresse'] ?? '')) ?></p>
|
||||
<p><strong><?php echo htmlspecialchars($translations['label_adresse']) ?> :</strong><br>
|
||||
<?php echo nl2br(htmlspecialchars($praticien['adresse'] ?? '')) ?></p>
|
||||
|
||||
<p><strong><?= htmlspecialchars($translations['label_presentation']) ?> :</strong><br>
|
||||
<?= nl2br(htmlspecialchars($praticien['presentation'] ?? '')) ?></p>
|
||||
<p><strong><?php echo htmlspecialchars($translations['label_presentation']) ?> :</strong><br>
|
||||
<?php echo nl2br(htmlspecialchars($presentationAffiche)) ?></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-center">
|
||||
<a href="/public/praticien/modifier.php" class="btn btn-primary">
|
||||
<?= htmlspecialchars($translations['btn_modifier_mes_infos']) ?>
|
||||
<?php echo htmlspecialchars($translations['btn_modifier_mes_infos']) ?>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -90,11 +117,6 @@ require_once __DIR__ . '/../includes/header.php';
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p><?= $translations['copyr'] ?></p>
|
||||
<p><?php echo $translations['copyr'] ?></p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<?php
|
||||
// Si ton header.php ouvre <body>, pense à fermer proprement dans footer.php.
|
||||
// Si tu n'as pas de footer commun, laisse comme ça.
|
||||
?>
|
||||
+548
-229
@@ -1,289 +1,617 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../includes/init_praticien.php';
|
||||
require_once __DIR__ . '/../includes/init_praticien.php';
|
||||
|
||||
$id = $_SESSION['reference_id'] ?? null;
|
||||
if (! $id) {
|
||||
http_response_code(403);
|
||||
exit('Forbidden');
|
||||
}
|
||||
|
||||
$id = $_SESSION['reference_id'] ?? null;
|
||||
if (!$id) { http_response_code(403); exit('Forbidden'); }
|
||||
$currentLang = $lang ?? 'ru';
|
||||
if (! in_array($currentLang, ['ru', 'fr', 'en'], true)) {
|
||||
$currentLang = 'ru';
|
||||
}
|
||||
$isRu = ($currentLang === 'ru');
|
||||
|
||||
// Récupérer les données (pour affichage initial & valeurs par défaut)
|
||||
$stmt = $pdo->prepare("SELECT * FROM praticien WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$praticien = $stmt->fetch();
|
||||
if (!$praticien) {
|
||||
$ro = $isRu ? '' : 'readonly';
|
||||
$dis = $isRu ? '' : 'disabled';
|
||||
|
||||
/**
|
||||
* Upsert traduction
|
||||
*/
|
||||
function upsertPraticienTraduction(PDO $pdo, int $praticienId, string $langue, array $data): void
|
||||
{
|
||||
$sql = "
|
||||
INSERT INTO praticien_traduction (
|
||||
praticien_id,
|
||||
langue,
|
||||
nom_affiche,
|
||||
prenom_affiche,
|
||||
ville_affichee,
|
||||
titre,
|
||||
specialite,
|
||||
presentation,
|
||||
disponibilite,
|
||||
statut,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (
|
||||
:praticien_id,
|
||||
:langue,
|
||||
:nom_affiche,
|
||||
:prenom_affiche,
|
||||
:ville_affichee,
|
||||
:titre,
|
||||
:specialite,
|
||||
:presentation,
|
||||
:disponibilite,
|
||||
:statut,
|
||||
NOW(),
|
||||
NOW()
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
nom_affiche = VALUES(nom_affiche),
|
||||
prenom_affiche = VALUES(prenom_affiche),
|
||||
ville_affichee = VALUES(ville_affichee),
|
||||
titre = VALUES(titre),
|
||||
specialite = VALUES(specialite),
|
||||
presentation = VALUES(presentation),
|
||||
disponibilite = VALUES(disponibilite),
|
||||
statut = VALUES(statut),
|
||||
updated_at = NOW()
|
||||
";
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute([
|
||||
'praticien_id' => $praticienId,
|
||||
'langue' => $langue,
|
||||
'nom_affiche' => $data['nom_affiche'],
|
||||
'prenom_affiche' => $data['prenom_affiche'],
|
||||
'ville_affichee' => $data['ville_affichee'],
|
||||
'titre' => $data['titre'],
|
||||
'specialite' => $data['specialite'],
|
||||
'presentation' => $data['presentation'],
|
||||
'disponibilite' => $data['disponibilite'],
|
||||
'statut' => $data['statut'],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lecture traduction par langue
|
||||
*/
|
||||
function getPraticienTraduction(PDO $pdo, int $praticienId, string $langue): ?array
|
||||
{
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT *
|
||||
FROM praticien_traduction
|
||||
WHERE praticien_id = ? AND langue = ?
|
||||
LIMIT 1
|
||||
");
|
||||
$stmt->execute([$praticienId, $langue]);
|
||||
$row = $stmt->fetch();
|
||||
return $row ?: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translittération simple cyrillique -> latin
|
||||
* Suffisant pour test / affichage.
|
||||
*/
|
||||
function transliterateToLatin(string $text): string
|
||||
{
|
||||
$map = [
|
||||
'А' => 'A', 'а' => 'a', 'Б' => 'B', 'б' => 'b', 'В' => 'V', 'в' => 'v', 'Г' => 'G', 'г' => 'g',
|
||||
'Д' => 'D', 'д' => 'd', 'Е' => 'E', 'е' => 'e', 'Ё' => 'Yo', 'ё' => 'yo', 'Ж' => 'Zh', 'ж' => 'zh',
|
||||
'З' => 'Z', 'з' => 'z', 'И' => 'I', 'и' => 'i', 'Й' => 'Y', 'й' => 'y', 'К' => 'K', 'к' => 'k',
|
||||
'Л' => 'L', 'л' => 'l', 'М' => 'M', 'м' => 'm', 'Н' => 'N', 'н' => 'n', 'О' => 'O', 'о' => 'o',
|
||||
'П' => 'P', 'п' => 'p', 'Р' => 'R', 'р' => 'r', 'С' => 'S', 'с' => 's', 'Т' => 'T', 'т' => 't',
|
||||
'У' => 'U', 'у' => 'u', 'Ф' => 'F', 'ф' => 'f', 'Х' => 'Kh', 'х' => 'kh', 'Ц' => 'Ts', 'ц' => 'ts',
|
||||
'Ч' => 'Ch', 'ч' => 'ch', 'Ш' => 'Sh', 'ш' => 'sh', 'Щ' => 'Shch', 'щ' => 'shch',
|
||||
'Ы' => 'Y', 'ы' => 'y', 'Э' => 'E', 'э' => 'e', 'Ю' => 'Yu', 'ю' => 'yu', 'Я' => 'Ya', 'я' => 'ya',
|
||||
'Ь' => '', 'ь' => '', 'Ъ' => '', 'ъ' => '',
|
||||
];
|
||||
return strtr($text, $map);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stub traduction.
|
||||
* Pour l’instant :
|
||||
* - renvoie le texte tel quel si pas de vrai moteur branché
|
||||
* - tu pourras remplacer ici par DeepL/API plus tard
|
||||
*/
|
||||
function translateText(string $text, string $sourceLang, string $targetLang): string
|
||||
{
|
||||
$text = trim($text);
|
||||
if ($text === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$sourceLang = strtoupper($sourceLang);
|
||||
$targetLang = strtoupper($targetLang);
|
||||
|
||||
if ($sourceLang === $targetLang) {
|
||||
return $text;
|
||||
}
|
||||
|
||||
if (! defined('DEEPL_API_KEY') || DEEPL_API_KEY === '') {
|
||||
throw new RuntimeException('Clé API DeepL manquante.');
|
||||
}
|
||||
|
||||
$endpoint = 'https://api-free.deepl.com/v2/translate';
|
||||
|
||||
$postFields = http_build_query([
|
||||
'text' => $text,
|
||||
'source_lang' => $sourceLang,
|
||||
'target_lang' => $targetLang,
|
||||
]);
|
||||
|
||||
$ch = curl_init($endpoint);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => $postFields,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 20,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Authorization: DeepL-Auth-Key ' . DEEPL_API_KEY,
|
||||
'Content-Type: application/x-www-form-urlencoded',
|
||||
],
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$curlErr = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($response === false || $curlErr) {
|
||||
throw new RuntimeException('Erreur réseau DeepL : ' . $curlErr);
|
||||
}
|
||||
|
||||
if ($httpCode < 200 || $httpCode >= 300) {
|
||||
throw new RuntimeException('Erreur DeepL HTTP ' . $httpCode . ' : ' . $response);
|
||||
}
|
||||
|
||||
$data = json_decode($response, true);
|
||||
|
||||
if (
|
||||
! is_array($data)
|
||||
|| empty($data['translations'])
|
||||
|| ! isset($data['translations'][0]['text'])
|
||||
) {
|
||||
throw new RuntimeException('Réponse DeepL invalide.');
|
||||
}
|
||||
|
||||
return trim($data['translations'][0]['text']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Détecte si une traduction est manuelle
|
||||
*/
|
||||
function isManualTranslation(?array $row): bool
|
||||
{
|
||||
return isset($row['statut']) && $row['statut'] === 'manuel';
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronise les traductions à partir de la source RU dans praticien
|
||||
* - ru toujours sync
|
||||
* - fr/en mis à jour seulement si absents ou non manuels
|
||||
*/
|
||||
function syncTranslationsFromSource(PDO $pdo, array $praticien): void
|
||||
{
|
||||
$praticienId = (int) $praticien['id'];
|
||||
|
||||
$ruData = [
|
||||
'nom_affiche' => $praticien['nom'] ?? '',
|
||||
'prenom_affiche' => $praticien['prenom'] ?? '',
|
||||
'ville_affichee' => $praticien['ville'] ?? '',
|
||||
'titre' => $praticien['titre'] ?? '',
|
||||
'specialite' => $praticien['specialite'] ?? '',
|
||||
'presentation' => $praticien['presentation'] ?? '',
|
||||
'disponibilite' => $praticien['disponibilite'] ?? '',
|
||||
'statut' => 'auto',
|
||||
];
|
||||
upsertPraticienTraduction($pdo, $praticienId, 'ru', $ruData);
|
||||
|
||||
foreach (['fr', 'en'] as $langue) {
|
||||
$existing = getPraticienTraduction($pdo, $praticienId, $langue);
|
||||
|
||||
if ($existing && isManualTranslation($existing)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$nomLatin = transliterateToLatin((string) ($praticien['nom'] ?? ''));
|
||||
$prenomLatin = transliterateToLatin((string) ($praticien['prenom'] ?? ''));
|
||||
|
||||
try {
|
||||
$data = [
|
||||
'nom_affiche' => $nomLatin,
|
||||
'prenom_affiche' => $prenomLatin,
|
||||
'ville_affichee' => translateText((string) ($praticien['ville'] ?? ''), 'RU', $langue),
|
||||
'titre' => translateText((string) ($praticien['titre'] ?? ''), 'RU', $langue),
|
||||
'specialite' => translateText((string) ($praticien['specialite'] ?? ''), 'RU', $langue),
|
||||
'presentation' => translateText((string) ($praticien['presentation'] ?? ''), 'RU', $langue),
|
||||
'disponibilite' => translateText((string) ($praticien['disponibilite'] ?? ''), 'RU', $langue),
|
||||
'statut' => 'auto',
|
||||
];
|
||||
} catch (Throwable $e) {
|
||||
error_log('DeepL translation error [' . $langue . '] praticien_id=' . $praticienId . ' : ' . $e->getMessage());
|
||||
//die($e->getMessage());
|
||||
|
||||
$data = [
|
||||
'nom_affiche' => $nomLatin,
|
||||
'prenom_affiche' => $prenomLatin,
|
||||
'ville_affichee' => (string) ($praticien['ville'] ?? ''),
|
||||
'titre' => (string) ($praticien['titre'] ?? ''),
|
||||
'specialite' => (string) ($praticien['specialite'] ?? ''),
|
||||
'presentation' => (string) ($praticien['presentation'] ?? ''),
|
||||
'disponibilite' => (string) ($praticien['disponibilite'] ?? ''),
|
||||
'statut' => 'erreur',
|
||||
];
|
||||
}
|
||||
|
||||
upsertPraticienTraduction($pdo, $praticienId, $langue, $data);
|
||||
}
|
||||
}
|
||||
|
||||
// Base praticien
|
||||
$stmt = $pdo->prepare("SELECT * FROM praticien WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$praticien = $stmt->fetch();
|
||||
|
||||
if (! $praticien) {
|
||||
die(htmlspecialchars($translations['praticien_introuvable'] ?? 'Introuvable.'));
|
||||
}
|
||||
}
|
||||
|
||||
// ==== Traitement POST avant tout output ====
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
// Traduction langue courante
|
||||
$traduction = getPraticienTraduction($pdo, (int) $id, $currentLang);
|
||||
|
||||
// Valeurs formulaire avec fallback
|
||||
$formNom = $traduction['nom_affiche'] ?? $praticien['nom'] ?? '';
|
||||
$formPrenom = $traduction['prenom_affiche'] ?? $praticien['prenom'] ?? '';
|
||||
$formVilleAffichee = $traduction['ville_affichee'] ?? $praticien['ville'] ?? '';
|
||||
$formTitre = $traduction['titre'] ?? $praticien['titre'] ?? '';
|
||||
$formSpecialite = $traduction['specialite'] ?? $praticien['specialite'] ?? '';
|
||||
$formPresentation = $traduction['presentation'] ?? $praticien['presentation'] ?? '';
|
||||
$formDispo = $traduction['disponibilite'] ?? $praticien['disponibilite'] ?? '';
|
||||
|
||||
// ==== POST ====
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
require_post_csrf();
|
||||
$titre = trim($_POST['titre'] ?? '');
|
||||
$nom = trim($_POST['nom'] ?? '');
|
||||
$prenom = trim($_POST['prenom'] ?? '');
|
||||
$nomAffiche = trim($_POST['nom'] ?? '');
|
||||
$prenomAffiche = trim($_POST['prenom'] ?? '');
|
||||
$telephone = trim($_POST['telephone'] ?? '');
|
||||
$specialite = trim($_POST['specialite'] ?? '');
|
||||
$adresse = trim($_POST['adresse'] ?? '');
|
||||
$ville = trim($_POST['ville'] ?? '');
|
||||
$code_postal = trim($_POST['code_postal'] ?? '');
|
||||
$presentation = trim($_POST['presentation'] ?? '');
|
||||
$duree_rdv = (int)($_POST['duree_rdv'] ?? 60);
|
||||
$duree_rdv = (int) ($_POST['duree_rdv'] ?? 60);
|
||||
$disponibilite = trim($_POST['disponibilite'] ?? '');
|
||||
$photo = $praticien['photo'] ?? null;
|
||||
|
||||
if ($specialite === '' || $presentation === '') {
|
||||
die(htmlspecialchars($translations['err_champs_obligatoires'] ?? 'Merci de remplir les champs obligatoires.'));
|
||||
}
|
||||
|
||||
// En FR/EN on ignore tout update des champs globaux même si quelqu’un bidouille le HTML
|
||||
if (! $isRu) {
|
||||
$telephone = $praticien['telephone'] ?? '';
|
||||
$adresse = $praticien['adresse'] ?? '';
|
||||
$ville = $praticien['ville'] ?? '';
|
||||
$code_postal = $praticien['code_postal'] ?? '';
|
||||
$duree_rdv = (int) ($praticien['duree_rdv'] ?? 60);
|
||||
}
|
||||
|
||||
// Upload photo : uniquement RU
|
||||
if ($isRu && ! empty($_FILES['photo']['tmp_name'])) {
|
||||
$file = $_FILES['photo'];
|
||||
$maxSize = 5 * 1024 * 1024;
|
||||
|
||||
// --- BLOC UPLOAD SÉCURISÉ ---
|
||||
if (!empty($_FILES['photo']['tmp_name'])) {
|
||||
$file = $_FILES['photo'];
|
||||
$maxSize = 5 * 1024 * 1024; // 5 Mo
|
||||
|
||||
// 1. Vérification de la taille
|
||||
if ($file['size'] > $maxSize) {
|
||||
die("Erreur : Le fichier est trop volumineux (max 5 Mo).");
|
||||
}
|
||||
|
||||
// 2. Vérification du type réel (MIME) pour bloquer les faux fichiers
|
||||
$finfo = new finfo(FILEINFO_MIME_TYPE);
|
||||
$mimeType = $finfo->file($file['tmp_name']);
|
||||
$finfo = new finfo(FILEINFO_MIME_TYPE);
|
||||
$mimeType = $finfo->file($file['tmp_name']);
|
||||
$allowedMimeTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
||||
|
||||
if (!in_array($mimeType, $allowedMimeTypes)) {
|
||||
if (! in_array($mimeType, $allowedMimeTypes, true)) {
|
||||
die("Erreur : Type de fichier non autorisé (Seuls JPG, PNG, GIF et WEBP sont acceptés).");
|
||||
}
|
||||
|
||||
// 3. Préparation du nom et du chemin
|
||||
$nom_nettoye = strtolower(preg_replace('/[^A-Za-z0-9]/', '', $nom));
|
||||
$prenom_nettoye = strtolower(preg_replace('/[^A-Za-z0-9]/', '', $prenom));
|
||||
$nom_nettoye = strtolower(preg_replace('/[^A-Za-z0-9]/', '', transliterateToLatin($nomAffiche)));
|
||||
$prenom_nettoye = strtolower(preg_replace('/[^A-Za-z0-9]/', '', transliterateToLatin($prenomAffiche)));
|
||||
$nouveau_nom_fichier = $nom_nettoye . '_' . $prenom_nettoye . '_' . time() . '.png';
|
||||
$chemin_destination = __DIR__ . '/../uploads/' . $nouveau_nom_fichier;
|
||||
$chemin_destination = __DIR__ . '/../uploads/' . $nouveau_nom_fichier;
|
||||
|
||||
// 4. Traitement de l'image (Conversion & Redimensionnement)
|
||||
$image_source = @imagecreatefromstring(file_get_contents($file['tmp_name']));
|
||||
|
||||
if ($image_source) {
|
||||
list($largeur_orig, $hauteur_orig) = getimagesize($file['tmp_name']);
|
||||
$largeur_max = 800;
|
||||
|
||||
if ($largeur_orig > $largeur_max) {
|
||||
$ratio = $largeur_max / $largeur_orig;
|
||||
$nouvelle_largeur = $largeur_max;
|
||||
$nouvelle_hauteur = (int)($hauteur_orig * $ratio);
|
||||
} else {
|
||||
$nouvelle_largeur = $largeur_orig;
|
||||
$nouvelle_hauteur = $hauteur_orig;
|
||||
}
|
||||
|
||||
$image_dest = imagecreatetruecolor($nouvelle_largeur, $nouvelle_hauteur);
|
||||
imagealphablending($image_dest, false);
|
||||
imagesavealpha($image_dest, true);
|
||||
|
||||
imagecopyresampled($image_dest, $image_source, 0, 0, 0, 0, $nouvelle_largeur, $nouvelle_hauteur, $largeur_orig, $hauteur_orig);
|
||||
|
||||
// Sauvegarde effective
|
||||
if (imagepng($image_dest, $chemin_destination, 7)) {
|
||||
// Suppression de l'ancienne photo si elle existe
|
||||
if (!empty($praticien['photo'])) {
|
||||
$ancien_chemin = __DIR__ . '/../uploads/' . $praticien['photo'];
|
||||
if (file_exists($ancien_chemin)) {
|
||||
unlink($ancien_chemin);
|
||||
}
|
||||
}
|
||||
$photo = $nouveau_nom_fichier;
|
||||
}
|
||||
|
||||
imagedestroy($image_source);
|
||||
imagedestroy($image_dest);
|
||||
} else {
|
||||
if (! $image_source) {
|
||||
die("Erreur : Le fichier image semble corrompu.");
|
||||
}
|
||||
}
|
||||
|
||||
// SIRET - normalisation + vérifs
|
||||
$siret = preg_replace('/\D/', '', $_POST['siret'] ?? '');
|
||||
if (strlen($siret) !== 14) {
|
||||
die(htmlspecialchars($translations['siret_format_invalide'] ?? 'SIRET invalide (14 chiffres).'));
|
||||
}
|
||||
// Luhn
|
||||
$luhnOk = (function($num){
|
||||
$sum=0; $alt=false;
|
||||
for ($i=strlen($num)-1; $i>=0; $i--){
|
||||
$n=(int)$num[$i];
|
||||
if($alt){ $n*=2; if($n>9)$n-=9; }
|
||||
$sum+=$n; $alt=!$alt;
|
||||
[$largeur_orig, $hauteur_orig] = getimagesize($file['tmp_name']);
|
||||
$largeur_max = 800;
|
||||
|
||||
if ($largeur_orig > $largeur_max) {
|
||||
$ratio = $largeur_max / $largeur_orig;
|
||||
$nouvelle_largeur = $largeur_max;
|
||||
$nouvelle_hauteur = (int) ($hauteur_orig * $ratio);
|
||||
} else {
|
||||
$nouvelle_largeur = $largeur_orig;
|
||||
$nouvelle_hauteur = $hauteur_orig;
|
||||
}
|
||||
return $sum%10===0;
|
||||
})($siret);
|
||||
if (!$luhnOk) {
|
||||
die(htmlspecialchars($translations['siret_format_invalide'] ?? 'SIRET invalide (Luhn).'));
|
||||
|
||||
$image_dest = imagecreatetruecolor($nouvelle_largeur, $nouvelle_hauteur);
|
||||
imagealphablending($image_dest, false);
|
||||
imagesavealpha($image_dest, true);
|
||||
|
||||
imagecopyresampled(
|
||||
$image_dest,
|
||||
$image_source,
|
||||
0, 0, 0, 0,
|
||||
$nouvelle_largeur,
|
||||
$nouvelle_hauteur,
|
||||
$largeur_orig,
|
||||
$hauteur_orig
|
||||
);
|
||||
|
||||
if (imagepng($image_dest, $chemin_destination, 7)) {
|
||||
if (! empty($praticien['photo'])) {
|
||||
$ancien_chemin = __DIR__ . '/../uploads/' . $praticien['photo'];
|
||||
if (file_exists($ancien_chemin)) {
|
||||
unlink($ancien_chemin);
|
||||
}
|
||||
}
|
||||
$photo = $nouveau_nom_fichier;
|
||||
}
|
||||
|
||||
imagedestroy($image_source);
|
||||
imagedestroy($image_dest);
|
||||
}
|
||||
|
||||
// Unicité SIRET (exclure l'utilisateur courant)
|
||||
$chk = $pdo->prepare("SELECT id FROM praticien WHERE siret = :s AND id <> :id LIMIT 1");
|
||||
$chk->execute(['s' => $siret, 'id' => $id]);
|
||||
if ($chk->fetch()) {
|
||||
die(htmlspecialchars($translations['err_siret_deja_utilise'] ?? 'Ce SIRET est déjà utilisé.'));
|
||||
$pdo->beginTransaction();
|
||||
|
||||
try {
|
||||
if ($isRu) {
|
||||
$siret = preg_replace('/\D/', '', $_POST['siret'] ?? '');
|
||||
|
||||
if (strlen($siret) !== 14) {
|
||||
throw new RuntimeException($translations['siret_format_invalide'] ?? 'SIRET invalide (14 chiffres).');
|
||||
}
|
||||
|
||||
$luhnOk = (function ($num) {
|
||||
$sum = 0;
|
||||
$alt = false;
|
||||
for ($i = strlen($num) - 1; $i >= 0; $i--) {
|
||||
$n = (int) $num[$i];
|
||||
if ($alt) {
|
||||
$n *= 2;
|
||||
if ($n > 9) {
|
||||
$n -= 9;
|
||||
}
|
||||
|
||||
}
|
||||
$sum += $n;
|
||||
$alt = ! $alt;
|
||||
}
|
||||
return $sum % 10 === 0;
|
||||
})($siret);
|
||||
|
||||
if (! $luhnOk) {
|
||||
throw new RuntimeException($translations['siret_format_invalide'] ?? 'SIRET invalide (Luhn).');
|
||||
}
|
||||
|
||||
$chk = $pdo->prepare("SELECT id FROM praticien WHERE siret = :s AND id <> :id LIMIT 1");
|
||||
$chk->execute(['s' => $siret, 'id' => $id]);
|
||||
if ($chk->fetch()) {
|
||||
throw new RuntimeException($translations['err_siret_deja_utilise'] ?? 'Ce SIRET est déjà utilisé.');
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE praticien SET
|
||||
titre = :titre,
|
||||
nom = :nom,
|
||||
prenom = :prenom,
|
||||
telephone = :telephone,
|
||||
specialite = :specialite,
|
||||
adresse = :adresse,
|
||||
ville = :ville,
|
||||
code_postal = :code_postal,
|
||||
presentation = :presentation,
|
||||
photo = :photo,
|
||||
disponibilite = :disponibilite,
|
||||
duree_rdv = :duree_rdv,
|
||||
siret = :siret,
|
||||
langue_source = 'ru'
|
||||
WHERE id = :id
|
||||
");
|
||||
$stmt->execute([
|
||||
'titre' => $titre,
|
||||
'nom' => $nomAffiche,
|
||||
'prenom' => $prenomAffiche,
|
||||
'telephone' => $telephone,
|
||||
'specialite' => $specialite,
|
||||
'adresse' => $adresse,
|
||||
'ville' => $ville,
|
||||
'code_postal' => $code_postal,
|
||||
'presentation' => $presentation,
|
||||
'photo' => $photo,
|
||||
'disponibilite' => $disponibilite,
|
||||
'duree_rdv' => $duree_rdv,
|
||||
'siret' => $siret,
|
||||
'id' => $id,
|
||||
]);
|
||||
|
||||
// Recharge la source après update
|
||||
$stmt = $pdo->prepare("SELECT * FROM praticien WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$praticien = $stmt->fetch();
|
||||
|
||||
if (! $praticien) {
|
||||
throw new RuntimeException('Praticien introuvable après mise à jour.');
|
||||
}
|
||||
|
||||
syncTranslationsFromSource($pdo, $praticien);
|
||||
} else {
|
||||
upsertPraticienTraduction($pdo, (int) $id, $currentLang, [
|
||||
'nom_affiche' => $nomAffiche,
|
||||
'prenom_affiche' => $prenomAffiche,
|
||||
'ville_affichee' => $traduction['ville_affichee'] ?? $praticien['ville'] ?? '',
|
||||
'titre' => $titre,
|
||||
'specialite' => $specialite,
|
||||
'presentation' => $presentation,
|
||||
'disponibilite' => $disponibilite,
|
||||
'statut' => 'manuel',
|
||||
]);
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
} catch (Throwable $e) {
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
die(htmlspecialchars($e->getMessage()));
|
||||
}
|
||||
|
||||
// Mise à jour
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE praticien SET
|
||||
titre = :titre,
|
||||
nom = :nom,
|
||||
prenom = :prenom,
|
||||
telephone = :telephone,
|
||||
specialite = :specialite,
|
||||
adresse = :adresse,
|
||||
ville = :ville,
|
||||
code_postal = :code_postal,
|
||||
presentation = :presentation,
|
||||
photo = :photo,
|
||||
disponibilite = :disponibilite,
|
||||
duree_rdv = :duree_rdv,
|
||||
siret = :siret
|
||||
WHERE id = :id
|
||||
");
|
||||
$stmt->execute([
|
||||
'titre' => $titre,
|
||||
'nom' => $nom,
|
||||
'prenom' => $prenom,
|
||||
'telephone' => $telephone,
|
||||
'specialite' => $specialite,
|
||||
'adresse' => $adresse,
|
||||
'ville' => $ville,
|
||||
'code_postal' => $code_postal,
|
||||
'presentation' => $presentation,
|
||||
'photo' => $photo,
|
||||
'disponibilite' => $disponibilite,
|
||||
'duree_rdv' => $duree_rdv,
|
||||
'siret' => $siret,
|
||||
'id' => $id,
|
||||
]);
|
||||
|
||||
// Redirection ABSOLUE (ou "informations.php" si même dossier)
|
||||
header('Location: /public/praticien/informations.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$PAGE_TITLE = $translations['modifier_infos_title'];
|
||||
$HEAD_EXTRA = <<<HTML
|
||||
<style>
|
||||
.message-erreur {
|
||||
color: red;
|
||||
font-size: 0.9em;
|
||||
display: none;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
$PAGE_TITLE = $translations['modifier_infos_title'];
|
||||
$HEAD_EXTRA = <<<HTML
|
||||
<style>
|
||||
.message-erreur {
|
||||
color: red;
|
||||
font-size: 0.9em;
|
||||
display: none;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
HTML;
|
||||
// ==== À partir d’ici, on peut afficher ====
|
||||
require_once __DIR__ . '/../includes/header.php';
|
||||
|
||||
require_once __DIR__ . '/../includes/header.php';
|
||||
?>
|
||||
|
||||
<div class="main1">
|
||||
<div class="container py-5">
|
||||
<form id="form-praticien-edit" method="POST" enctype="multipart/form-data" class="shadow p-4 rounded bg-light">
|
||||
<h2 class="text-center mb-4"><?= htmlspecialchars($translations['modifier_infos_title']) ?></h2>
|
||||
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(ensure_csrf_token(), ENT_QUOTES, 'UTF-8'); ?>">
|
||||
<h2 class="text-center mb-4"><?php echo htmlspecialchars($translations['modifier_infos_title']) ?></h2>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= htmlspecialchars($translations['label_nom']) ?> *</label>
|
||||
<input type="text" name="nom" class="form-control" value="<?= htmlspecialchars($praticien['nom']?? '') ?>" required>
|
||||
<label class="form-label"><?php echo htmlspecialchars($translations['label_nom']) ?> *</label>
|
||||
<input type="text" name="nom" class="form-control" value="<?php echo htmlspecialchars($formNom) ?>" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= htmlspecialchars($translations['label_prenom']) ?> *</label>
|
||||
<input type="text" name="prenom" class="form-control" value="<?= htmlspecialchars($praticien['prenom']?? '') ?>" required>
|
||||
<label class="form-label"><?php echo htmlspecialchars($translations['label_prenom']) ?> *</label>
|
||||
<input type="text" name="prenom" class="form-control" value="<?php echo htmlspecialchars($formPrenom) ?>" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= htmlspecialchars($translations['label_telephone']) ?> *</label>
|
||||
<input type="tel" name="telephone" id="telephone" class="form-control" value="<?= htmlspecialchars($praticien['telephone']?? '') ?>" required placeholder="+33600000000"
|
||||
|
||||
pattern="^\+?[1-9]\d{6,14}$"
|
||||
title="<?=htmlspecialchars($translations['telephone_format_message']?? '') ?>">
|
||||
<label class="form-label"><?php echo htmlspecialchars($translations['label_telephone']) ?> *</label>
|
||||
<input type="tel" name="telephone" id="telephone" class="form-control"
|
||||
value="<?php echo htmlspecialchars($praticien['telephone'] ?? '') ?>"
|
||||
required
|
||||
placeholder="+33600000000"
|
||||
pattern="^\+?[1-9]\d{6,14}$"
|
||||
title="<?php echo htmlspecialchars($translations['telephone_format_message'] ?? '') ?>"
|
||||
<?php echo $ro; ?>>
|
||||
</div>
|
||||
<!-- Champ SIREN/SIRET -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= htmlspecialchars($translations['placeholder_siret']) ?> *</label>
|
||||
<input type="text"
|
||||
name="siret"
|
||||
id="siret"
|
||||
class="form-control"
|
||||
value="<?= htmlspecialchars($praticien['siret']?? '') ?>"
|
||||
required
|
||||
maxlength="17"
|
||||
inputmode="numeric"
|
||||
pattern="[0-9 ]+">
|
||||
<div id="siret-feedback" class="message-erreur" style="display:none;"></div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= htmlspecialchars($translations['label_specialite']) ?> *</label>
|
||||
<input type="text" name="specialite" class="form-control" value="<?= htmlspecialchars($praticien['specialite']?? '') ?>" required>
|
||||
<label class="form-label"><?php echo htmlspecialchars($translations['placeholder_siret']) ?> *</label>
|
||||
<input type="text"
|
||||
name="siret"
|
||||
id="siret"
|
||||
class="form-control"
|
||||
value="<?php echo htmlspecialchars($praticien['siret'] ?? '') ?>"
|
||||
required
|
||||
maxlength="17"
|
||||
inputmode="numeric"
|
||||
pattern="[0-9 ]+"
|
||||
>
|
||||
<div id="siret-feedback" class="message-erreur" style="display:none;"></div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= htmlspecialchars($translations['tarif_consultation']) ?></label>
|
||||
<input type="text" name="titre" class="form-control" value="<?= htmlspecialchars($praticien['titre']?? '') ?>">
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?php echo htmlspecialchars($translations['label_specialite']) ?> *</label>
|
||||
<input type="text" name="specialite" class="form-control" value="<?php echo htmlspecialchars($formSpecialite) ?>" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= htmlspecialchars($translations['moyens_payement']) ?></label>
|
||||
<input type="text" name="disponibilite" class="form-control" value="<?= htmlspecialchars($praticien['disponibilite']?? '') ?>">
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?php echo htmlspecialchars($translations['tarif_consultation']) ?></label>
|
||||
<input type="text" name="titre" class="form-control" value="<?php echo htmlspecialchars($formTitre) ?>">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?php echo htmlspecialchars($translations['moyens_payement']) ?></label>
|
||||
<input type="text" name="disponibilite" class="form-control" value="<?php echo htmlspecialchars($formDispo) ?>">
|
||||
</div>
|
||||
|
||||
<div class="mb-3 position-relative">
|
||||
<label class="form-label"><?= htmlspecialchars($translations['label_adresse']) ?></label>
|
||||
<textarea name="adresse" id="adresse" class="form-control"><?= htmlspecialchars($praticien['adresse']?? '') ?></textarea>
|
||||
<label class="form-label"><?php echo htmlspecialchars($translations['label_adresse']) ?></label>
|
||||
<textarea name="adresse" id="adresse" class="form-control" <?php echo $ro; ?>><?php echo htmlspecialchars($praticien['adresse'] ?? '') ?></textarea>
|
||||
<ul id="suggestions"></ul>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= htmlspecialchars($translations['label_ville']) ?></label>
|
||||
<input type="text" name="ville" id="ville" class="form-control" value="<?= htmlspecialchars($praticien['ville']?? '') ?>">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= htmlspecialchars($translations['label_code_postal']) ?></label>
|
||||
<input type="text" name="code_postal" id="code_postal" class="form-control" value="<?= htmlspecialchars($praticien['code_postal']?? '') ?>">
|
||||
</div>
|
||||
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= htmlspecialchars($translations['label_presentation']) ?></label>
|
||||
<textarea name="presentation" class="form-control"><?= htmlspecialchars($praticien['presentation']?? '') ?></textarea>
|
||||
<label class="form-label"><?php echo htmlspecialchars($translations['label_ville']) ?></label>
|
||||
<input type="text" name="ville" id="ville" class="form-control"
|
||||
value="<?php echo htmlspecialchars($praticien['ville'] ?? '') ?>" readonly>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= htmlspecialchars($translations['label_duree_rdv']) ?></label>
|
||||
<input type="number" name="duree_rdv" class="form-control" value="<?= $praticien['duree_rdv'] ?>" min="15" max="180" required>
|
||||
<label class="form-label"><?php echo htmlspecialchars($translations['label_code_postal']) ?></label>
|
||||
<input type="text" name="code_postal" id="code_postal" class="form-control"
|
||||
value="<?php echo htmlspecialchars($praticien['code_postal'] ?? '') ?>"
|
||||
<?php echo $ro; ?>>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= htmlspecialchars($translations['label_photo']) ?></label>
|
||||
<input type="file" name="photo" class="form-control" accept="image/*">
|
||||
<label class="form-label"><?php echo htmlspecialchars($translations['label_presentation']) ?> *</label>
|
||||
<textarea name="presentation" class="form-control" required><?php echo htmlspecialchars($formPresentation) ?></textarea>
|
||||
</div>
|
||||
|
||||
<?php if ($praticien['photo']): ?>
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?php echo htmlspecialchars($translations['label_duree_rdv']) ?></label>
|
||||
<input type="number" name="duree_rdv" class="form-control"
|
||||
value="<?php echo (int) ($praticien['duree_rdv'] ?? 60); ?>"
|
||||
min="15" max="180" required
|
||||
<?php echo $ro; ?>>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?php echo htmlspecialchars($translations['label_photo']) ?></label>
|
||||
<input type="file" name="photo" class="form-control" accept="image/*" <?php echo $dis; ?>>
|
||||
</div>
|
||||
|
||||
<?php if (! empty($praticien['photo'])): ?>
|
||||
<div class="mb-3">
|
||||
<p><?= htmlspecialchars($translations['photo_actuelle']) ?></p>
|
||||
<img src="/public/uploads/<?= htmlspecialchars($praticien['photo']) ?>" class="img-fluid rounded-4 shadow" style="width:230px;height:auto;">
|
||||
<p><?php echo htmlspecialchars($translations['photo_actuelle']) ?></p>
|
||||
<img src="/public/uploads/<?php echo htmlspecialchars($praticien['photo']) ?>"
|
||||
class="img-fluid rounded-4 shadow"
|
||||
style="width:230px;height:auto;">
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="mt-4 text-center">
|
||||
<button type="submit" class="btn btn-primary"><?= htmlspecialchars($translations['btn_enregistrer']) ?></button>
|
||||
|
||||
|
||||
<div class="mt-4 text-center">
|
||||
<button type="submit" class="btn btn-primary"><?php echo htmlspecialchars($translations['btn_enregistrer']) ?></button>
|
||||
</div>
|
||||
<?= $translations['champs_obligatoire'] ?>
|
||||
|
||||
|
||||
<?php echo $translations['champs_obligatoire'] ?>
|
||||
</form>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
// === Téléphone: simple & efficace ===
|
||||
const tel = document.getElementById('telephone');
|
||||
|
||||
// 1) Nettoyage en direct (espaces, tirets, points, parenthèses)
|
||||
tel?.addEventListener('input', function () {
|
||||
const pos = this.selectionStart;
|
||||
this.value = this.value.replace(/[\s\-._()/]/g, '');
|
||||
try { this.setSelectionRange(pos, pos); } catch(_) {}
|
||||
});
|
||||
|
||||
// 2) Normalisation au blur
|
||||
tel?.addEventListener('blur', function() {
|
||||
let val = this.value.trim().replace(/[\s\-._()/]/g, '');
|
||||
if (val.startsWith('00')) val = '+' + val.slice(2);
|
||||
@@ -292,7 +620,6 @@ tel?.addEventListener('blur', function() {
|
||||
this.value = val;
|
||||
});
|
||||
|
||||
// 3) Au submit : applique la normalisation même si l’utilisateur n’a pas quitté le champ
|
||||
tel?.form?.addEventListener('submit', function (e) {
|
||||
tel.dispatchEvent(new Event('blur'));
|
||||
if (!tel.checkValidity()) {
|
||||
@@ -304,23 +631,24 @@ tel?.form?.addEventListener('submit', function (e) {
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const adresseInput = document.getElementById("adresse");
|
||||
const villeInput = document.getElementById("ville");
|
||||
const cpInput = document.getElementById("code_postal");
|
||||
const suggestions = document.getElementById("suggestions");
|
||||
const villeInput = document.getElementById("ville");
|
||||
const cpInput = document.getElementById("code_postal");
|
||||
const suggestions = document.getElementById("suggestions");
|
||||
|
||||
if (adresseInput.hasAttribute('readonly')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// petite fonction utilitaire : construit "numero + rue"
|
||||
function buildStreet(props) {
|
||||
const num = (props.housenumber || '').trim();
|
||||
const street = (props.street || props.name || '').trim();
|
||||
return [num, street].filter(Boolean).join(' ');
|
||||
}
|
||||
// fallback ville si city manquant (rare)
|
||||
|
||||
function getCity(props) {
|
||||
if (props.city) return props.city;
|
||||
// ex: "84160, Cucuron, Vaucluse, Provence-Alpes-Côte d'Azur"
|
||||
if (props.context) {
|
||||
const parts = props.context.split(',').map(s => s.trim());
|
||||
// généralement la 2e valeur correspond à la commune
|
||||
return parts[1] || parts[0] || '';
|
||||
}
|
||||
return '';
|
||||
@@ -335,7 +663,6 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
return;
|
||||
}
|
||||
|
||||
// annule la requête précédente si on retape vite
|
||||
try { abortCtrl?.abort(); } catch(_) {}
|
||||
abortCtrl = new AbortController();
|
||||
|
||||
@@ -352,9 +679,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
li.textContent = props.label || buildStreet(props);
|
||||
|
||||
li.addEventListener("click", function () {
|
||||
// ✅ Adresse = seulement numéro + rue
|
||||
adresseInput.value = buildStreet(props);
|
||||
// ✅ Ville + CP séparés
|
||||
villeInput.value = getCity(props);
|
||||
cpInput.value = props.postcode || '';
|
||||
suggestions.innerHTML = '';
|
||||
@@ -366,7 +691,6 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
.catch(() => {});
|
||||
});
|
||||
|
||||
// clic hors zone -> ferme la liste
|
||||
document.addEventListener("click", function (e) {
|
||||
if (!suggestions.contains(e.target) && e.target !== adresseInput) {
|
||||
suggestions.innerHTML = '';
|
||||
@@ -375,11 +699,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
});
|
||||
</script>
|
||||
|
||||
<!--***********************************************************
|
||||
Verification ajax pour SIRET
|
||||
**************************************************************-->
|
||||
<script>
|
||||
// Luhn SIRET
|
||||
function luhnCheck(num) {
|
||||
const arr = (num + '').split('').reverse().map(x => parseInt(x, 10));
|
||||
const sum = arr.reduce((acc, val, i) => {
|
||||
@@ -390,21 +710,24 @@ function luhnCheck(num) {
|
||||
}
|
||||
|
||||
(function(){
|
||||
const form = document.getElementById('form-praticien-edit'); // <-- bon id
|
||||
const form = document.getElementById('form-praticien-edit');
|
||||
const siretInput = document.getElementById('siret');
|
||||
const feedback = document.getElementById('siret-feedback');
|
||||
|
||||
// init
|
||||
if (!siretInput || siretInput.hasAttribute('readonly')) {
|
||||
return;
|
||||
}
|
||||
|
||||
form.dataset.siretExists = '0';
|
||||
let debounceId = null;
|
||||
let lastQueried = '';
|
||||
|
||||
const MSG = {
|
||||
invalide: '<?= addslashes($translations["siret_format_invalide"] ?? "Numéro SIRET invalide.") ?>',
|
||||
valide: '<?= addslashes($translations["siret_valide_format"] ?? "Format SIRET valide.") ?>',
|
||||
existe: '<?= addslashes($translations["siret_trouve"] ?? "Numéro reconnu dans la base INSEE.") ?>',
|
||||
nexiste: '<?= addslashes($translations["siret_non_trouve"] ?? "Numéro introuvable dans la base INSEE.") ?>',
|
||||
attente: '<?= addslashes($translations["siret_verification_en_cours"] ?? "Vérification en cours…") ?>'
|
||||
invalide: '<?php echo addslashes($translations["siret_format_invalide"] ?? "Numéro SIRET invalide.") ?>',
|
||||
valide: '<?php echo addslashes($translations["siret_valide_format"] ?? "Format SIRET valide.") ?>',
|
||||
existe: '<?php echo addslashes($translations["siret_trouve"] ?? "Numéro reconnu dans la base INSEE.") ?>',
|
||||
nexiste: '<?php echo addslashes($translations["siret_non_trouve"] ?? "Numéro introuvable dans la base INSEE.") ?>',
|
||||
attente: '<?php echo addslashes($translations["siret_verification_en_cours"] ?? "Vérification en cours…") ?>'
|
||||
};
|
||||
|
||||
function formatSiret(value) {
|
||||
@@ -414,13 +737,13 @@ function luhnCheck(num) {
|
||||
if (value.length > 11) value = value.slice(0,11) + ' ' + value.slice(11);
|
||||
return value;
|
||||
}
|
||||
|
||||
function setFeedback(text, color) {
|
||||
feedback.textContent = text;
|
||||
feedback.style.color = color;
|
||||
feedback.style.display = 'block';
|
||||
}
|
||||
|
||||
// Pré-formate la valeur initiale si elle est brute (14 chiffres)
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (/^\d{14}$/.test(siretInput.value)) {
|
||||
siretInput.value = formatSiret(siretInput.value);
|
||||
@@ -428,7 +751,6 @@ function luhnCheck(num) {
|
||||
});
|
||||
|
||||
function checkSiretImmediate() {
|
||||
// formatage visuel
|
||||
const formatted = formatSiret(siretInput.value);
|
||||
if (siretInput.value !== formatted) siretInput.value = formatted;
|
||||
|
||||
@@ -446,7 +768,7 @@ function luhnCheck(num) {
|
||||
|
||||
clearTimeout(debounceId);
|
||||
debounceId = setTimeout(() => {
|
||||
fetch('/../../js/ajax_verif_siret.php?siret=' + encodeURIComponent(raw)) // <-- chemin ABSOLU
|
||||
fetch('/../../js/ajax_verif_siret.php?siret=' + encodeURIComponent(raw))
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data && data.exists) {
|
||||
@@ -466,7 +788,6 @@ function luhnCheck(num) {
|
||||
|
||||
siretInput.addEventListener('input', checkSiretImmediate);
|
||||
|
||||
// Normalise AVANT envoi (supprime les espaces)
|
||||
form.addEventListener('submit', function(e){
|
||||
const raw = siretInput.value.replace(/\s+/g, '');
|
||||
if (raw.length !== 14 || !luhnCheck(raw)) {
|
||||
@@ -475,9 +796,7 @@ function luhnCheck(num) {
|
||||
siretInput.focus();
|
||||
return;
|
||||
}
|
||||
siretInput.value = raw; // <- on poste 14 chiffres à PHP
|
||||
// Si tu veux forcer l’existence INSEE côté front, décommente :
|
||||
// if (form.dataset.siretExists !== '1') { e.preventDefault(); setFeedback(MSG.nexiste, 'red'); siretInput.focus(); }
|
||||
siretInput.value = raw;
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@@ -485,7 +804,7 @@ function luhnCheck(num) {
|
||||
</div>
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p><?= $translations['copyr'] ?></p>
|
||||
<p><?php echo $translations['copyr'] ?></p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
|
||||
require_once __DIR__ . '/../includes/init_praticien.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'message' => 'Méthode non autorisée.',
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
require_post_csrf();
|
||||
|
||||
$praticien_id = (int) ($_SESSION['reference_id'] ?? 0);
|
||||
if ($praticien_id <= 0) {
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'message' => 'Praticien non autorisé.',
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
$id = (int) ($_POST['id'] ?? 0);
|
||||
if ($id <= 0) {
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'message' => 'ID invalide.',
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$check = $pdo->prepare("
|
||||
SELECT id, praticien_id
|
||||
FROM disponibilite
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
");
|
||||
$check->execute([$id]);
|
||||
$slot = $check->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (! $slot) {
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'message' => 'Créneau introuvable.',
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ((int) $slot['praticien_id'] !== $praticien_id) {
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'message' => 'Ce créneau ne vous appartient pas.',
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("DELETE FROM disponibilite WHERE id = ? AND praticien_id = ?");
|
||||
$stmt->execute([$id, $praticien_id]);
|
||||
|
||||
echo json_encode([
|
||||
'ok' => ($stmt->rowCount() > 0),
|
||||
'message' => $stmt->rowCount() > 0
|
||||
? 'Créneau supprimé.'
|
||||
: 'Aucune ligne supprimée.',
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
|
||||
} catch (Throwable $e) {
|
||||
error_log('supprimer_disponibilite.php: ' . $e->getMessage());
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'message' => 'Erreur serveur.',
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
|
||||
require_once __DIR__ . '/../includes/init_praticien.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'message' => 'Méthode non autorisée.',
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
require_post_csrf();
|
||||
|
||||
$praticien_id = (int) ($_SESSION['reference_id'] ?? 0);
|
||||
if ($praticien_id <= 0) {
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'message' => 'Accès refusé.',
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
$id = (int) ($_POST['id'] ?? 0);
|
||||
if ($id <= 0) {
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'message' => 'ID invalide.',
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$check = $pdo->prepare("
|
||||
SELECT id, praticien_id
|
||||
FROM disponibilite_date
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
");
|
||||
$check->execute([$id]);
|
||||
$slot = $check->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (! $slot) {
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'message' => 'Créneau introuvable.',
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ((int) $slot['praticien_id'] !== $praticien_id) {
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'message' => 'Ce créneau ne vous appartient pas.',
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("DELETE FROM disponibilite_date WHERE id = ? AND praticien_id = ?");
|
||||
$stmt->execute([$id, $praticien_id]);
|
||||
|
||||
echo json_encode([
|
||||
'ok' => ($stmt->rowCount() > 0),
|
||||
'message' => $stmt->rowCount() > 0
|
||||
? 'Créneau supprimé.'
|
||||
: 'Aucune ligne supprimée.',
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
|
||||
} catch (Throwable $e) {
|
||||
error_log('praticien/supprimer_disponibilite_date.php: ' . $e->getMessage());
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'message' => 'Erreur serveur.',
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
@@ -0,0 +1,576 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
|
||||
require_once __DIR__ . '/../includes/bootstrap_global.php';
|
||||
|
||||
// ===== Helpers header (identiques à index.php) =====
|
||||
function is_client_logged_in(): bool
|
||||
{
|
||||
$role = $_SESSION['role'] ?? ($_SESSION['user_type'] ?? '');
|
||||
|
||||
if (! empty($_SESSION['user_id']) && $role === 'client') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (! empty($_SESSION['client_id'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (! empty($_SESSION['reference_id']) && $role === 'client') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (! empty($_SESSION['user_id']) && $role === 'praticien') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (! empty($_SESSION['praticien_id'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (! empty($_SESSION['reference_id']) && $role === 'praticien') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function include_header_clean(string $absPath): void
|
||||
{
|
||||
global $translations, $lang;
|
||||
|
||||
ob_start();
|
||||
include $absPath;
|
||||
$buf = ob_get_clean() ?: '';
|
||||
|
||||
$buf = preg_replace('~<!doctype\s+html[^>]*>~i', '', $buf);
|
||||
$buf = preg_replace('~</?html\b[^>]*>~i', '', $buf);
|
||||
|
||||
$buf = preg_replace('~<head\b[^>]*>.*?</head>~is', '', $buf);
|
||||
$buf = preg_replace('~</?body\b[^>]*>~i', '', $buf);
|
||||
|
||||
$buf = preg_replace('~<link[^>]+href="[^"]*(bootstrap[^"]*\.css|mobil_style\.css|site_style\.css|fonts\.googleapis)[^"]*"[^>]*>~i', '', $buf);
|
||||
|
||||
$buf = preg_replace('~<script[^>]+src="[^"]*bootstrap[^"]*bundle[^"]*\.js[^"]*"[^>]*>\s*</script>~i', '', $buf);
|
||||
|
||||
echo $buf;
|
||||
}
|
||||
|
||||
function t(array $translations, string $key, string $fallback = ''): string
|
||||
{
|
||||
$val = $translations[$key] ?? '';
|
||||
return $val === '' ? $fallback : (string) $val;
|
||||
}
|
||||
|
||||
// Lang HTML
|
||||
$langHtml = preg_match('~^[a-z]{2}(-[A-Z]{2})?$~', (string) ($lang ?? '')) ? (string) $lang : 'ru';
|
||||
|
||||
// SEO
|
||||
$metaTitle = t($translations, 'regist_meta_title', 'Регистрация специалиста на USLUGI.FR');
|
||||
$metaDescription = t($translations, 'regist_meta_description', 'Создайте профиль специалиста на USLUGI.FR и получайте новых клиентов во Франции.');
|
||||
$metaKeywords = t($translations, 'regist_meta_keywords', 'специалист, регистрация, профиль, клиенты, France, uslugi');
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="<?php echo htmlspecialchars($langHtml, ENT_QUOTES, 'UTF-8'); ?>">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title><?php echo htmlspecialchars($metaTitle, ENT_QUOTES, 'UTF-8'); ?></title>
|
||||
<meta name="description" content="<?php echo htmlspecialchars($metaDescription, ENT_QUOTES, 'UTF-8'); ?>">
|
||||
<meta name="keywords" content="<?php echo htmlspecialchars($metaKeywords, ENT_QUOTES, 'UTF-8'); ?>">
|
||||
<meta name="author" content="Uslugi.fr">
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="<?php echo asset('/includes/site_style.css'); ?>">
|
||||
<link rel="stylesheet" href="<?php echo asset('/includes/custom_style.css'); ?>">
|
||||
<link rel="stylesheet" href="<?php echo asset('/includes/mobil_style.css'); ?>">
|
||||
|
||||
<?php include_once __DIR__ . '/includes/tracking.php'; ?>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<?php if (is_client_logged_in()): ?>
|
||||
<?php
|
||||
require_once __DIR__ . '/public/includes/init_pr_cl.php';
|
||||
include_header_clean(__DIR__ . '/public/includes/header.php');
|
||||
?>
|
||||
<?php else: ?>
|
||||
<header class="site-header">
|
||||
<div class="container">
|
||||
<div class="header-left">
|
||||
<div class="brand">
|
||||
<a href="/index.php">
|
||||
<img src="/public/uploads/img/logo_u.png" alt="uslugi.fr" class="logo-mobile">
|
||||
</a>
|
||||
|
||||
<a href="/index.php">
|
||||
<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>
|
||||
|
||||
<nav class="header-actions">
|
||||
<a class="btn-login" href="/connexion.php">
|
||||
<span class="d-sm-none"><?php echo htmlspecialchars($translations['btn_se_connecter'] ?? 'Connexion', ENT_QUOTES, 'UTF-8'); ?></span>
|
||||
<span class="d-none d-sm-inline"><?php echo htmlspecialchars($translations['ja_spec'] ?? 'Je suis spécialiste', ENT_QUOTES, 'UTF-8'); ?></span>
|
||||
</a>
|
||||
|
||||
<div class="dropdown">
|
||||
<button
|
||||
class="btn btn-logo rounded-circle d-inline-flex align-items-center justify-content-center question-btn"
|
||||
type="button"
|
||||
data-bs-toggle="dropdown"
|
||||
data-bs-auto-close="true"
|
||||
aria-expanded="false"
|
||||
aria-label="<?php echo htmlspecialchars(t($translations, 'about_help_aria', 'Help'), ENT_QUOTES, 'UTF-8'); ?>">
|
||||
?
|
||||
</button>
|
||||
|
||||
<ul class="dropdown-menu dropdown-menu-end shadow help-menu menu-panel">
|
||||
<li><a href="/how-it-work.php"><?php echo htmlspecialchars($translations['kak_rabotaet'] ?? 'Как это работает', ENT_QUOTES, 'UTF-8'); ?></a></li>
|
||||
<li><a href="/about.php"><?php echo htmlspecialchars($translations['pro_nas'] ?? 'О нас', ENT_QUOTES, 'UTF-8'); ?></a></li>
|
||||
<li><a href="/faq.php"><?php echo htmlspecialchars($translations['faq'] ?? 'FAQ', ENT_QUOTES, 'UTF-8'); ?></a></li>
|
||||
<li><a href="/contact.php"><?php echo htmlspecialchars($translations['nam_pisat'] ?? 'Написать нам', ENT_QUOTES, 'UTF-8'); ?></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
<?php endif; ?>
|
||||
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
:root {
|
||||
--navy: #1a2744;
|
||||
--blue: #6b7fd4;
|
||||
--blue-light: #eef0fb;
|
||||
--red: #e03e3e;
|
||||
--white: #ffffff;
|
||||
--gray-bg: #f4f6fb;
|
||||
--text: #1a2744;
|
||||
--muted: #6b7a90;
|
||||
--border: #dde2f0;
|
||||
}
|
||||
|
||||
.content {
|
||||
max-width: 900px;
|
||||
background: #F2F3F7;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.hero {
|
||||
background: var(--white);
|
||||
padding: 48px 24px 40px;
|
||||
text-align: center;
|
||||
max-width: 640px;
|
||||
margin: 0 auto;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: inline-block;
|
||||
margin-bottom: 14px;
|
||||
padding: 6px 14px;
|
||||
border-radius: 999px;
|
||||
background: var(--blue-light);
|
||||
color: var(--navy);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
font-size: clamp(26px, 6vw, 36px);
|
||||
font-weight: 800;
|
||||
color: var(--navy);
|
||||
line-height: 1.2;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.hero p {
|
||||
font-size: 16px;
|
||||
color: var(--muted);
|
||||
max-width: 420px;
|
||||
margin: 0 auto 32px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.section {
|
||||
|
||||
padding: 40px 24px;
|
||||
}
|
||||
|
||||
.section-inner {
|
||||
max-width: 640px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.section h2 {
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
color: var(--navy);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.benefit-item {
|
||||
background: var(--white);
|
||||
border-radius: 16px;
|
||||
padding: 20px 24px;
|
||||
margin-bottom: 12px;
|
||||
border-left: 3px solid var(--blue);
|
||||
}
|
||||
|
||||
.benefit-item h3 {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: var(--navy);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.benefit-item p {
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.pricing {
|
||||
padding: 40px 24px;
|
||||
max-width: 640px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.pricing h2 {
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
color: var(--navy);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.pricing-sub {
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.pricing-cards {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.pricing-card {
|
||||
flex: 1;
|
||||
border: 2px solid var(--border);
|
||||
border-radius: 16px;
|
||||
padding: 24px 20px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.pricing-card.featured {
|
||||
border-color: var(--blue);
|
||||
background: var(--blue-light);
|
||||
}
|
||||
|
||||
.pricing-badge {
|
||||
position: absolute;
|
||||
top: -12px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: var(--navy);
|
||||
color: white;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
padding: 3px 12px;
|
||||
border-radius: 100px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pricing-period {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.pricing-price {
|
||||
font-size: 38px;
|
||||
font-weight: 800;
|
||||
color: var(--navy);
|
||||
line-height: 1;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.pricing-unit {
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
display: block;
|
||||
border: 2px solid var(--red);
|
||||
color: var(--red);
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
padding: 10px 16px;
|
||||
border-radius: 100px;
|
||||
text-decoration: none;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn-filled {
|
||||
display: block;
|
||||
background: var(--red);
|
||||
color: white;
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
padding: 10px 16px;
|
||||
border-radius: 100px;
|
||||
text-decoration: none;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.pricing-note {
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
margin-top: 16px;
|
||||
text-align: center;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.steps {
|
||||
|
||||
padding: 40px 24px;
|
||||
}
|
||||
|
||||
.steps-inner {
|
||||
max-width: 640px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.steps h2 {
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
color: var(--navy);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.step {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.step-num {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: var(--blue);
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 800;
|
||||
font-size: 15px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.step h3 {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: var(--navy);
|
||||
margin-bottom: 2px;
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
.step p {
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.help-box {
|
||||
background: var(--white);
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
margin-top: 8px;
|
||||
font-size: 14px;
|
||||
color: var(--muted);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.help-box strong {
|
||||
color: var(--navy);
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.cta {
|
||||
padding: 40px 24px 56px;
|
||||
text-align: center;
|
||||
max-width: 640px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.cta h2 {
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
color: var(--navy);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.cta p {
|
||||
font-size: 14px;
|
||||
color: var(--muted);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="main">
|
||||
<div class="content">
|
||||
<br>
|
||||
<div class="hero">
|
||||
<div class="tag"><?php echo htmlspecialchars(t($translations, 'regist_tag', 'Для специалистов'), ENT_QUOTES, 'UTF-8'); ?></div>
|
||||
<h1><?php echo nl2br(htmlspecialchars(t($translations, 'regist_h1', "Вы специалист?\nРазместите профиль на USLUGI.FR"), ENT_QUOTES, 'UTF-8')); ?></h1>
|
||||
<p><?php echo htmlspecialchars(t($translations, 'regist_hero_text', 'Получайте новых клиентов во Франции!'), ENT_QUOTES, 'UTF-8'); ?></p>
|
||||
<a href="/inscription-praticien" class="btn btn-primary">
|
||||
<?php echo htmlspecialchars(t($translations, 'regist_btn_create_profile', 'Создать профиль'), ENT_QUOTES, 'UTF-8'); ?>
|
||||
</a>
|
||||
</div>
|
||||
<br>
|
||||
<div class="section">
|
||||
<div class="section-inner">
|
||||
<h2><?php echo htmlspecialchars(t($translations, 'regist_benefits_title', 'Что вы получаете'), ENT_QUOTES, 'UTF-8'); ?></h2>
|
||||
|
||||
<div class="benefit-item">
|
||||
<div>
|
||||
<h3><?php echo htmlspecialchars(t($translations, 'regist_benefit_1_title', 'Профиль с вашей информацией'), ENT_QUOTES, 'UTF-8'); ?></h3>
|
||||
<p><?php echo htmlspecialchars(t($translations, 'regist_benefit_1_text', 'Специализация, город, фото, описание.'), ENT_QUOTES, 'UTF-8'); ?></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="benefit-item">
|
||||
<div>
|
||||
<h3><?php echo htmlspecialchars(t($translations, 'regist_benefit_2_title', 'Календарь записи'), ENT_QUOTES, 'UTF-8'); ?></h3>
|
||||
<p><?php echo htmlspecialchars(t($translations, 'regist_benefit_2_text', 'Клиент видит ваше свободное время и записывается сам.'), ENT_QUOTES, 'UTF-8'); ?></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="benefit-item">
|
||||
<div>
|
||||
<h3><?php echo htmlspecialchars(t($translations, 'regist_benefit_3_title', 'Видеосвязь на платформе'), ENT_QUOTES, 'UTF-8'); ?></h3>
|
||||
<p><?php echo htmlspecialchars(t($translations, 'regist_benefit_3_text', 'Онлайн-консультации без Zoom и лишних настроек.'), ENT_QUOTES, 'UTF-8'); ?></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="benefit-item">
|
||||
<div>
|
||||
<h3><?php echo htmlspecialchars(t($translations, 'regist_benefit_4_title', 'Чат с клиентом'), ENT_QUOTES, 'UTF-8'); ?></h3>
|
||||
<p><?php echo htmlspecialchars(t($translations, 'regist_benefit_4_text', 'Обменивайтесь документами и уточняйте детали до встречи.'), ENT_QUOTES, 'UTF-8'); ?></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="benefit-item">
|
||||
<div>
|
||||
<h3><?php echo htmlspecialchars(t($translations, 'regist_benefit_5_title', 'Целевая аудитория'), ENT_QUOTES, 'UTF-8'); ?></h3>
|
||||
<p><?php echo htmlspecialchars(t($translations, 'regist_benefit_5_text', 'Люди приходят с конкретным запросом. Часть записывается сами — часть мы направляем напрямую к вам через форму подбора.'), ENT_QUOTES, 'UTF-8'); ?></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pricing" id="pricing">
|
||||
<h2><?php echo htmlspecialchars(t($translations, 'regist_pricing_title', 'Стоимость'), ENT_QUOTES, 'UTF-8'); ?></h2>
|
||||
<p class="pricing-sub"><?php echo htmlspecialchars(t($translations, 'regist_pricing_sub', 'Без комиссии с консультаций — оплата напрямую от клиента'), ENT_QUOTES, 'UTF-8'); ?></p>
|
||||
|
||||
<div class="pricing-cards">
|
||||
<div class="pricing-card featured">
|
||||
<div class="pricing-badge"><?php echo htmlspecialchars(t($translations, 'regist_pricing_badge', 'Выгоднее'), ENT_QUOTES, 'UTF-8'); ?></div>
|
||||
<div class="pricing-period"><?php echo htmlspecialchars(t($translations, 'regist_yearly_label', 'В год'), ENT_QUOTES, 'UTF-8'); ?></div>
|
||||
<div class="pricing-price"><?php echo htmlspecialchars(t($translations, 'regist_yearly_price', '275€'), ENT_QUOTES, 'UTF-8'); ?></div>
|
||||
<div class="pricing-unit"><?php echo htmlspecialchars(t($translations, 'regist_yearly_unit', '23€ в месяц'), ENT_QUOTES, 'UTF-8'); ?></div>
|
||||
<a href="/inscription-praticien" class="btn-filled">
|
||||
<?php echo htmlspecialchars(t($translations, 'regist_btn_choose', 'Выбрать'), ENT_QUOTES, 'UTF-8'); ?>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="pricing-card">
|
||||
<div class="pricing-period"><?php echo htmlspecialchars(t($translations, 'regist_monthly_label', 'В месяц'), ENT_QUOTES, 'UTF-8'); ?></div>
|
||||
<div class="pricing-price"><?php echo htmlspecialchars(t($translations, 'regist_monthly_price', '25€'), ENT_QUOTES, 'UTF-8'); ?></div>
|
||||
<div class="pricing-unit"><?php echo htmlspecialchars(t($translations, 'regist_monthly_unit', ''), ENT_QUOTES, 'UTF-8'); ?> </div>
|
||||
<a href="/inscription-praticien" class="btn-outline">
|
||||
<?php echo htmlspecialchars(t($translations, 'regist_btn_choose', 'Выбрать'), ENT_QUOTES, 'UTF-8'); ?>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="pricing-note"><?php echo htmlspecialchars(t($translations, 'regist_pricing_note', 'Оплата картой прямо на сайте через Stripe'), ENT_QUOTES, 'UTF-8'); ?></p>
|
||||
</div>
|
||||
|
||||
<div class="steps">
|
||||
<div class="steps-inner">
|
||||
<h2><?php echo htmlspecialchars(t($translations, 'regist_steps_title', 'Как начать'), ENT_QUOTES, 'UTF-8'); ?></h2>
|
||||
|
||||
<div class="step">
|
||||
<div class="step-num">1</div>
|
||||
<div>
|
||||
<h3><?php echo htmlspecialchars(t($translations, 'regist_step_1_title', 'Выберите подписку и оплатите картой'), ENT_QUOTES, 'UTF-8'); ?></h3>
|
||||
<p><?php echo htmlspecialchars(t($translations, 'regist_step_1_text', 'Прямо на сайте, быстро и безопасно.'), ENT_QUOTES, 'UTF-8'); ?></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step">
|
||||
<div class="step-num">2</div>
|
||||
<div>
|
||||
<h3><?php echo htmlspecialchars(t($translations, 'regist_step_2_title', 'Создайте профиль'), ENT_QUOTES, 'UTF-8'); ?></h3>
|
||||
<p><?php echo htmlspecialchars(t($translations, 'regist_step_2_text', 'Фото, описание, специализация, календарь — всё сами.'), ENT_QUOTES, 'UTF-8'); ?></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step">
|
||||
<div class="step-num">3</div>
|
||||
<div>
|
||||
<h3><?php echo htmlspecialchars(t($translations, 'regist_step_3_title', 'Получайте запросы от клиентов'), ENT_QUOTES, 'UTF-8'); ?></h3>
|
||||
<p><?php echo htmlspecialchars(t($translations, 'regist_step_3_text', 'Уведомления на почту, запись в календарь, видеосвязь.'), ENT_QUOTES, 'UTF-8'); ?></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-box">
|
||||
<strong><?php echo htmlspecialchars(t($translations, 'regist_help_title', 'Нет времени заполнять профиль?'), ENT_QUOTES, 'UTF-8'); ?></strong>
|
||||
<?php echo htmlspecialchars(t($translations, 'regist_help_text', 'После оплаты пришлите нам фото и информацию — мы оформим за вас.'), ENT_QUOTES, 'UTF-8'); ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cta">
|
||||
<h2><?php echo htmlspecialchars(t($translations, 'regist_cta_title', 'Готовы начать?'), ENT_QUOTES, 'UTF-8'); ?></h2>
|
||||
<p><?php echo htmlspecialchars(t($translations, 'regist_cta_text', 'Регистрация занимает 5 минут'), ENT_QUOTES, 'UTF-8'); ?></p>
|
||||
<a href="/inscription-praticien" class="btn btn-primary">
|
||||
<?php echo htmlspecialchars(t($translations, 'regist_btn_create_profile', 'Создать профиль'), ENT_QUOTES, 'UTF-8'); ?>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p><?php echo $translations['copyr'] ?></p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -177,23 +177,19 @@ try {
|
||||
|
||||
$token = bin2hex(random_bytes(32));
|
||||
$token_validation_expire = (new DateTime('+6 hours'))->format('Y-m-d H:i:s');
|
||||
$token_autologin = bin2hex(random_bytes(32));
|
||||
$token_autologin_expire = (new DateTime('+60 minutes'))->format('Y-m-d H:i:s');
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE compte
|
||||
SET token_validation = :token,
|
||||
token_validation_expire = :token_expire,
|
||||
token_autologin = :al,
|
||||
token_autologin_expire = :al_exp
|
||||
WHERE id = :id
|
||||
LIMIT 1
|
||||
");
|
||||
UPDATE compte
|
||||
SET token_validation = :token,
|
||||
token_validation_expire = :token_expire,
|
||||
token_autologin = NULL,
|
||||
token_autologin_expire = NULL
|
||||
WHERE id = :id
|
||||
LIMIT 1
|
||||
");
|
||||
$stmt->execute([
|
||||
'token' => $token,
|
||||
'token_expire' => $token_validation_expire,
|
||||
'al' => $token_autologin,
|
||||
'al_exp' => $token_autologin_expire,
|
||||
'id' => (int) $compte['id'],
|
||||
]);
|
||||
|
||||
@@ -209,7 +205,7 @@ try {
|
||||
);
|
||||
}
|
||||
|
||||
$validation_url = "https://uslugi.fr/validation.php?token=" . urlencode($token) . "&al=" . urlencode($token_autologin);
|
||||
$validation_url = "https://uslugi.fr/validation.php?token=" . urlencode($token);
|
||||
$validation_url_html = htmlspecialchars($validation_url, ENT_QUOTES, 'UTF-8');
|
||||
|
||||
$prenom_nom = trim($prenom . ' ' . $nom);
|
||||
|
||||
+292
-69
@@ -5,8 +5,100 @@
|
||||
require_once APP_HTDOCS . '/includes/avatar_initiales.php';
|
||||
require_once __DIR__ . '/includes/search_helpers.php';
|
||||
|
||||
$currentPath = parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH) ?? '';
|
||||
|
||||
// Redirection de la recherche libre vers URL SEO
|
||||
if (
|
||||
($currentPath === '/recherche' || $currentPath === '/recherche/' || $currentPath === '/result_search.php')
|
||||
&& empty($_GET['ajax'])
|
||||
) {
|
||||
$qsSpecialite = trim((string) ($_GET['specialite'] ?? ''));
|
||||
$qsVille = trim((string) ($_GET['ville'] ?? ''));
|
||||
|
||||
if ($qsSpecialite !== '' || $qsVille !== '') {
|
||||
$target = '/recherche';
|
||||
|
||||
$specSlug = '';
|
||||
if ($qsSpecialite === '__OTHERS__') {
|
||||
$specSlug = 'autres-specialistes';
|
||||
} elseif ($qsSpecialite !== '') {
|
||||
$specSlug = seo_spec_slug_from_input($qsSpecialite) ?? '';
|
||||
}
|
||||
|
||||
$citySlug = '';
|
||||
if ($qsVille !== '') {
|
||||
$citySlug = seo_city_slug_from_input($qsVille);
|
||||
}
|
||||
|
||||
if ($specSlug !== '') {
|
||||
$target .= '/' . rawurlencode($specSlug);
|
||||
}
|
||||
|
||||
if ($specSlug !== '') {
|
||||
if ($citySlug !== '') {
|
||||
$target .= '/' . rawurlencode($citySlug);
|
||||
}
|
||||
|
||||
$target .= '/';
|
||||
|
||||
$extra = $_GET;
|
||||
unset($extra['specialite'], $extra['ville'], $extra['slug']);
|
||||
|
||||
if (! empty($extra)) {
|
||||
$target .= '?' . http_build_query($extra);
|
||||
}
|
||||
|
||||
if (($_SERVER['REQUEST_URI'] ?? '') !== $target) {
|
||||
header('Location: ' . $target, true, 301);
|
||||
exit;
|
||||
}
|
||||
} elseif ($citySlug !== '') {
|
||||
$target = '/recherche/ville/' . rawurlencode($citySlug) . '/';
|
||||
|
||||
$extra = $_GET;
|
||||
unset($extra['specialite'], $extra['ville'], $extra['slug']);
|
||||
|
||||
if (! empty($extra)) {
|
||||
$target .= '?' . http_build_query($extra);
|
||||
}
|
||||
|
||||
if (($_SERVER['REQUEST_URI'] ?? '') !== $target) {
|
||||
header('Location: ' . $target, true, 301);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$SPEC_CANON = require __DIR__ . '/includes/spec_map.php';
|
||||
$canon = $SPEC_CANON[$lang] ?? [];
|
||||
//Ajout 1 pourpraticien_traduction
|
||||
$currentLang = $lang ?? 'ru';
|
||||
if (! in_array($currentLang, ['ru', 'fr', 'en'], true)) {
|
||||
$currentLang = 'ru';
|
||||
}
|
||||
|
||||
// Lecture et normalisation des filtres
|
||||
$specialiteParam = trim($_GET['specialite'] ?? '');
|
||||
$ville = trim($_GET['ville'] ?? '');
|
||||
|
||||
// 🔑 On garde le slug SEO ici
|
||||
$resolvedSeoSpecSlug = null;
|
||||
|
||||
// Remap de la spécialité URL/texte vers la valeur de recherche
|
||||
if ($specialiteParam !== '' && $specialiteParam !== '__OTHERS__') {
|
||||
|
||||
$seoSpecSlug = seo_spec_slug_from_input($specialiteParam);
|
||||
|
||||
if ($seoSpecSlug !== null) {
|
||||
$resolvedSeoSpecSlug = $seoSpecSlug; // ⭐ IMPORTANT
|
||||
|
||||
$searchSpec = seo_spec_search_value_from_slug($seoSpecSlug, $lang);
|
||||
if ($searchSpec !== null) {
|
||||
$specialiteParam = $searchSpec;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$H1_LABELS = [
|
||||
'ru' => [
|
||||
@@ -46,25 +138,21 @@
|
||||
|
||||
$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"];
|
||||
}
|
||||
|
||||
// Direct file (mp4/webm/ogg) — no reliable thumb
|
||||
$path = parse_url($u, PHP_URL_PATH) ?? '';
|
||||
if (preg_match('~\.(mp4|webm|ogg)$~i', $path)) {
|
||||
return ['provider' => 'file', 'id' => null, 'thumb' => null];
|
||||
}
|
||||
|
||||
// Vimeo and others — fallback placeholder
|
||||
if (strpos(strtolower($u), 'vimeo.com') !== false) {
|
||||
return ['provider' => 'vimeo', 'id' => null, 'thumb' => null];
|
||||
}
|
||||
@@ -72,12 +160,8 @@
|
||||
return ['provider' => 'other', 'id' => null, 'thumb' => null];
|
||||
}
|
||||
|
||||
// Lecture et normalisation des filtres
|
||||
$specialiteParam = trim($_GET['specialite'] ?? '');
|
||||
$ville = trim($_GET['ville'] ?? '');
|
||||
|
||||
// inverse map: canon_value -> key (avocats, vrachi, ...)
|
||||
$canonToKey = [];
|
||||
// inverse map: canon_value -> key
|
||||
$canonToKey = [];
|
||||
foreach (($SPEC_CANON[$lang] ?? []) as $k => $v) {
|
||||
$canonToKey[mb_strtolower($v, 'UTF-8')] = $k;
|
||||
}
|
||||
@@ -86,7 +170,7 @@
|
||||
$h1Default = $translations['res_poisk'] ?? 'Результаты поиска';
|
||||
$h1 = $h1Default;
|
||||
|
||||
// On normalise l'affichage du terme (pour l'utiliser plus tard si besoin)
|
||||
// Normalisation affichage terme
|
||||
$sp = trim(str_replace(["\xC2\xA0"], ' ', $specialiteParam));
|
||||
$sp = preg_replace('/\s+/u', ' ', $sp);
|
||||
$sp_l = mb_strtolower($sp, 'UTF-8');
|
||||
@@ -94,14 +178,33 @@
|
||||
if ($specialiteParam === '__OTHERS__') {
|
||||
$label = $H1_LABELS[$lang]['drugie'] ?? $h1Default;
|
||||
$h1 = $label . $inFrance;
|
||||
} elseif (! empty($resolvedSeoSpecSlug)) {
|
||||
$slugToKey = [
|
||||
'avocat' => 'avocats',
|
||||
'consultant' => 'consultants',
|
||||
'medecin' => 'vrachi',
|
||||
'psychologue' => 'psychologues',
|
||||
'professeur' => 'repetitori',
|
||||
'comptable' => 'finance',
|
||||
'traducteur' => 'perevod',
|
||||
'notaire' => 'notarius',
|
||||
'guide' => 'gid',
|
||||
'agent-immobilier' => 'rieltori',
|
||||
'autres-specialistes' => 'drugie',
|
||||
];
|
||||
|
||||
$h1Key = $slugToKey[$resolvedSeoSpecSlug] ?? null;
|
||||
if ($h1Key !== null) {
|
||||
$label = $H1_LABELS[$lang][$h1Key] ?? $h1Default;
|
||||
$h1 = $label . $inFrance;
|
||||
}
|
||||
} elseif ($sp !== '' && isset($canonToKey[$sp_l])) {
|
||||
$key = $canonToKey[$sp_l];
|
||||
$label = $H1_LABELS[$lang][$key] ?? ($translations[$key] ?? $sp);
|
||||
$h1 = $label . $inFrance;
|
||||
}
|
||||
// sinon: on laisse $h1Default, on décidera après la requête SQL (selon résultats)
|
||||
|
||||
// Cas spécial : "Другие специалисты" (chip "drugie")
|
||||
// Cas spécial : autres spécialistes
|
||||
$onlyOthers = false;
|
||||
$specialite = $specialiteParam;
|
||||
|
||||
@@ -110,7 +213,7 @@
|
||||
$specialite = '';
|
||||
}
|
||||
|
||||
// Pagination simple
|
||||
// Pagination
|
||||
$perPage = 12;
|
||||
$page = max(1, (int) ($_GET['page'] ?? 1));
|
||||
$offset = ($page - 1) * $perPage;
|
||||
@@ -119,36 +222,36 @@
|
||||
$where = [];
|
||||
$params = [];
|
||||
|
||||
/*==========================BLOCK WHERE AVEC ABONNEMENT VALIDE ==========================*/
|
||||
$where[] = "(date_expiration IS NOT NULL AND date_expiration >= DATE_SUB(NOW(), INTERVAL 14 DAY))";
|
||||
/*=======================================================================================*/
|
||||
/*========================== BLOCK WHERE AVEC ABONNEMENT VALIDE ==========================*/
|
||||
$where[] = "(p.date_expiration IS NOT NULL AND p.date_expiration >= DATE_SUB(NOW(), INTERVAL 14 DAY))";
|
||||
/*========================================================================================*/
|
||||
|
||||
// 🔎 Unifie: le champ "specialite" peut contenir une spécialité OU un nom/prénom
|
||||
$term = trim((string) $specialite);
|
||||
$term_l = mb_strtolower($term, 'UTF-8');
|
||||
// Le champ "specialite" peut contenir une spécialité OU un nom/prénom
|
||||
$term = trim((string) $specialite);
|
||||
$term_l = mb_strtolower($term, 'UTF-8');
|
||||
|
||||
// 1) Cas "Другие": tout sauf les grandes catégories
|
||||
// 1) Cas "Другие"
|
||||
if ($onlyOthers) {
|
||||
$mainSpecs = array_values($canon);
|
||||
if ($mainSpecs) {
|
||||
$ors = [];
|
||||
foreach ($mainSpecs as $i => $spc) {
|
||||
$key = ":cat{$i}";
|
||||
$ors[] = "LOWER(specialite) LIKE $key";
|
||||
$ors[] = "LOWER(p.specialite) LIKE $key";
|
||||
$params[$key] = mb_strtolower($spc, 'UTF-8') . '%';
|
||||
}
|
||||
$where[] = '(specialite IS NULL OR specialite = "" OR NOT (' . implode(' OR ', $ors) . '))';
|
||||
$where[] = '(p.specialite IS NULL OR p.specialite = "" OR NOT (' . implode(' OR ', $ors) . '))';
|
||||
}
|
||||
}
|
||||
// 2) Cas normal : recherche par spécialité / nom / prénom
|
||||
// 2) Cas normal
|
||||
elseif ($term !== '') {
|
||||
$where[] =
|
||||
"("
|
||||
. "LOWER(specialite) LIKE :sp_like "
|
||||
. "OR LOWER(nom) LIKE :nm_prefix1 "
|
||||
. "OR LOWER(prenom) LIKE :nm_prefix2 "
|
||||
. "OR LOWER(CONCAT(prenom,' ',nom)) LIKE :nm_like_full1 "
|
||||
. "OR LOWER(CONCAT(nom,' ',prenom)) LIKE :nm_like_full2 "
|
||||
. "LOWER(COALESCE(pt.specialite, p.specialite)) LIKE :sp_like "
|
||||
. "OR LOWER(COALESCE(pt.nom_affiche, p.nom)) LIKE :nm_prefix1 "
|
||||
. "OR LOWER(COALESCE(pt.prenom_affiche, p.prenom)) LIKE :nm_prefix2 "
|
||||
. "OR LOWER(CONCAT(COALESCE(pt.prenom_affiche, p.prenom),' ',COALESCE(pt.nom_affiche, p.nom))) LIKE :nm_like_full1 "
|
||||
. "OR LOWER(CONCAT(COALESCE(pt.nom_affiche, p.nom),' ',COALESCE(pt.prenom_affiche, p.prenom))) LIKE :nm_like_full2 "
|
||||
. ")";
|
||||
$params[':sp_like'] = '%' . $term_l . '%';
|
||||
$params[':nm_prefix1'] = $term_l . '%';
|
||||
@@ -157,7 +260,7 @@
|
||||
$params[':nm_like_full2'] = '%' . $term_l . '%';
|
||||
}
|
||||
|
||||
// Filtre ville — support latin + cyrillique (alias + translit)
|
||||
// Filtre ville — support latin + cyrillique
|
||||
if ($ville !== '') {
|
||||
$villeCands = city_query_candidates($ville);
|
||||
$villeCands[] = mb_strtolower($ville, 'UTF-8');
|
||||
@@ -167,7 +270,7 @@
|
||||
foreach ($villeCands as $i => $cand) {
|
||||
$key = ":ville{$i}";
|
||||
$params[$key] = $cand . '%';
|
||||
$villeOrs[] = "ville LIKE {$key}";
|
||||
$villeOrs[] = "LOWER(COALESCE(pt.ville_affichee, p.ville)) LIKE {$key}";
|
||||
}
|
||||
if ($villeOrs) {
|
||||
$where[] = '(' . implode(' OR ', $villeOrs) . ')';
|
||||
@@ -177,25 +280,54 @@
|
||||
$whereSql = $where ? ('WHERE ' . implode(' AND ', $where)) : '';
|
||||
|
||||
// total
|
||||
$sqlCount = "SELECT COUNT(*) FROM praticien $whereSql";
|
||||
$stmt = $pdo->prepare($sqlCount);
|
||||
$stmt->execute($params);
|
||||
$sqlCount = "
|
||||
SELECT COUNT(*)
|
||||
FROM praticien p
|
||||
LEFT JOIN praticien_traduction pt
|
||||
ON pt.praticien_id = p.id
|
||||
AND pt.langue = :lang
|
||||
$whereSql
|
||||
";
|
||||
|
||||
$stmt = $pdo->prepare($sqlCount);
|
||||
|
||||
$paramsCount = $params;
|
||||
$paramsCount[':lang'] = $currentLang;
|
||||
|
||||
$stmt->execute($paramsCount);
|
||||
$total = (int) $stmt->fetchColumn();
|
||||
$pages = max(1, (int) ceil($total / $perPage));
|
||||
|
||||
// data
|
||||
$sql = "SELECT id, nom, prenom, specialite, ville, photo, presentation, video
|
||||
FROM praticien
|
||||
$whereSql";
|
||||
$sql = "
|
||||
SELECT
|
||||
p.id,
|
||||
p.nom,
|
||||
p.prenom,
|
||||
p.ville,
|
||||
p.photo,
|
||||
p.video,
|
||||
COALESCE(pt.nom_affiche, p.nom) AS nom_affiche,
|
||||
COALESCE(pt.prenom_affiche, p.prenom) AS prenom_affiche,
|
||||
COALESCE(pt.specialite, p.specialite) AS specialite_affichee,
|
||||
COALESCE(pt.ville_affichee, p.ville) AS ville_affichee,
|
||||
COALESCE(pt.presentation, p.presentation) AS presentation_affichee
|
||||
FROM praticien p
|
||||
LEFT JOIN praticien_traduction pt
|
||||
ON pt.praticien_id = p.id
|
||||
AND pt.langue = :lang
|
||||
$whereSql
|
||||
";
|
||||
|
||||
if ($term !== '') {
|
||||
$sql .= " ORDER BY
|
||||
(LOWER(CONCAT(prenom,' ',nom)) = :nm_exact1
|
||||
OR LOWER(CONCAT(nom,' ',prenom)) = :nm_exact2) DESC,
|
||||
(LOWER(nom) = :nm_exact3 OR LOWER(prenom) = :nm_exact4) DESC,
|
||||
(LOWER(nom) LIKE :nm_prefix3 OR LOWER(prenom) LIKE :nm_prefix4) DESC,
|
||||
(LOWER(specialite) LIKE :sp_prefix1) DESC,
|
||||
nom ASC, prenom ASC";
|
||||
(LOWER(CONCAT(COALESCE(pt.prenom_affiche, p.prenom),' ',COALESCE(pt.nom_affiche, p.nom))) = :nm_exact1
|
||||
OR LOWER(CONCAT(COALESCE(pt.nom_affiche, p.nom),' ',COALESCE(pt.prenom_affiche, p.prenom))) = :nm_exact2) DESC,
|
||||
(LOWER(COALESCE(pt.nom_affiche, p.nom)) = :nm_exact3 OR LOWER(COALESCE(pt.prenom_affiche, p.prenom)) = :nm_exact4) DESC,
|
||||
(LOWER(COALESCE(pt.nom_affiche, p.nom)) LIKE :nm_prefix3 OR LOWER(COALESCE(pt.prenom_affiche, p.prenom)) LIKE :nm_prefix4) DESC,
|
||||
(LOWER(COALESCE(pt.specialite, p.specialite)) LIKE :sp_prefix1) DESC,
|
||||
COALESCE(pt.nom_affiche, p.nom) ASC,
|
||||
COALESCE(pt.prenom_affiche, p.prenom) ASC";
|
||||
$params[':nm_exact1'] = $term_l;
|
||||
$params[':nm_exact2'] = $term_l;
|
||||
$params[':nm_exact3'] = $term_l;
|
||||
@@ -204,14 +336,15 @@
|
||||
$params[':nm_prefix4'] = $term_l . '%';
|
||||
$params[':sp_prefix1'] = $term_l . '%';
|
||||
} else {
|
||||
$sql .= " ORDER BY updated_at DESC, id DESC";
|
||||
$sql .= " ORDER BY p.updated_at DESC, p.id DESC";
|
||||
}
|
||||
|
||||
$sql .= " LIMIT :offset, :limit";
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
|
||||
// Lie tous les :villeN / :specialite
|
||||
$params[':lang'] = $currentLang;
|
||||
|
||||
foreach ($params as $k => $v) {
|
||||
$stmt->bindValue($k, $v, PDO::PARAM_STR);
|
||||
}
|
||||
@@ -221,15 +354,16 @@
|
||||
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// ✅ Si ce n'est pas une catégorie canon / pas __OTHERS__
|
||||
// et qu'on a des résultats => on affiche "Résultats pour : X"
|
||||
if ($h1 === $h1Default && $specialiteParam !== '__OTHERS__' && $sp !== '' && ! isset($canonToKey[$sp_l])) {
|
||||
// Si ce n'est pas une catégorie canonique et qu'on a des résultats
|
||||
if (
|
||||
$h1 === $h1Default
|
||||
&& $specialiteParam !== '__OTHERS__'
|
||||
&& $sp !== ''
|
||||
&& empty($resolvedSeoSpecSlug)
|
||||
&& ! isset($canonToKey[$sp_l])
|
||||
) {
|
||||
if (! empty($rows)) {
|
||||
if ($lang === 'fr') {
|
||||
$h1 = ''; //'Résultats de recherche pour : ' . $sp;
|
||||
} else {
|
||||
$h1 = ''; //'Результаты поиска по: ' . $sp;
|
||||
}
|
||||
$h1 = '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,15 +397,103 @@
|
||||
}
|
||||
|
||||
$baseQuery = http_build_query($qParams);
|
||||
|
||||
// =========================
|
||||
// SEO dynamique
|
||||
// =========================
|
||||
$seoSpecialite = trim((string) $specialiteParam);
|
||||
$seoVille = trim((string) $ville);
|
||||
|
||||
$isCanonCategory = ($seoSpecialite !== '' && isset($canonToKey[mb_strtolower($seoSpecialite, 'UTF-8')]));
|
||||
$isOthersCategory = ($seoSpecialite === '__OTHERS__');
|
||||
|
||||
$seoDisplaySpecialite = $seoSpecialite;
|
||||
if ($isOthersCategory) {
|
||||
$seoDisplaySpecialite = $translations['drugie'] ?? 'Spécialistes';
|
||||
}
|
||||
|
||||
if ($seoDisplaySpecialite === '') {
|
||||
$seoTitle = $translations['boostseo_search_title_default'] ?? '';
|
||||
$seoDesc = $translations['boostseo_search_desc_default'] ?? '';
|
||||
$seoIntro = $translations['boostseo_search_intro_default'] ?? '';
|
||||
} elseif ($seoVille !== '') {
|
||||
$seoTitle = strtr(($translations['boostseo_search_title_spec_city'] ?? ''), [
|
||||
'%specialite%' => $seoDisplaySpecialite,
|
||||
'%ville%' => $seoVille,
|
||||
]);
|
||||
$seoDesc = strtr(($translations['boostseo_search_desc_spec_city'] ?? ''), [
|
||||
'%specialite%' => $seoDisplaySpecialite,
|
||||
'%ville%' => $seoVille,
|
||||
]);
|
||||
$seoIntro = strtr(($translations['boostseo_search_intro_spec_city'] ?? ''), [
|
||||
'%specialite%' => $seoDisplaySpecialite,
|
||||
'%ville%' => $seoVille,
|
||||
]);
|
||||
} else {
|
||||
$seoTitle = strtr(($translations['boostseo_search_title_spec'] ?? ''), [
|
||||
'%specialite%' => $seoDisplaySpecialite,
|
||||
]);
|
||||
$seoDesc = strtr(($translations['boostseo_search_desc_spec'] ?? ''), [
|
||||
'%specialite%' => $seoDisplaySpecialite,
|
||||
]);
|
||||
$seoIntro = strtr(($translations['boostseo_search_intro_spec'] ?? ''), [
|
||||
'%specialite%' => $seoDisplaySpecialite,
|
||||
]);
|
||||
}
|
||||
|
||||
$canonicalUrl = 'https://uslugi.fr/recherche';
|
||||
|
||||
$canonicalSpecSlug = '';
|
||||
if ($specialiteParam === '__OTHERS__') {
|
||||
$canonicalSpecSlug = 'autres-specialistes';
|
||||
} elseif ($specialiteParam !== '') {
|
||||
foreach (seo_spec_map() as $slug => $cfg) {
|
||||
if (($cfg['db'] ?? '') === $specialiteParam) {
|
||||
$canonicalSpecSlug = $slug;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$cityCanonicalSlug = '';
|
||||
if ($ville !== '') {
|
||||
$cityCanonicalSlug = seo_city_slug_from_input($ville);
|
||||
}
|
||||
|
||||
if ($canonicalSpecSlug !== '') {
|
||||
$canonicalUrl .= '/' . rawurlencode($canonicalSpecSlug);
|
||||
if ($cityCanonicalSlug !== '') {
|
||||
$canonicalUrl .= '/' . rawurlencode($cityCanonicalSlug);
|
||||
}
|
||||
} elseif ($cityCanonicalSlug !== '') {
|
||||
$canonicalUrl .= '/ville/' . rawurlencode($cityCanonicalSlug);
|
||||
}
|
||||
|
||||
$canonicalUrl .= '/';
|
||||
|
||||
if ($page > 1) {
|
||||
$canonicalUrl .= '?page=' . $page;
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title><?php echo htmlspecialchars($translations['seo_result_search_title'] ?? '', ENT_QUOTES, 'UTF-8') ?></title>
|
||||
<meta name="description" content="<?php echo htmlspecialchars($translations['seo_result_search_desc'] ?? '', ENT_QUOTES, 'UTF-8') ?>">
|
||||
<meta name="keywords" content="<?php echo htmlspecialchars($translations['seo_result_search_keywords'] ?? '', ENT_QUOTES, 'UTF-8') ?>">
|
||||
<meta name="author" content="Uslugi.fr">
|
||||
<title><?php echo h($seoTitle) ?></title>
|
||||
<meta name="description" content="<?php echo h($seoDesc) ?>">
|
||||
<meta name="author" content="Uslugi.fr">
|
||||
<meta name="robots" content="index, follow">
|
||||
<link rel="canonical" href="<?php echo h($canonicalUrl) ?>">
|
||||
|
||||
<link rel="alternate" hreflang="ru" href="<?php echo h($canonicalUrl . (str_contains($canonicalUrl, '?') ? '&lang=ru' : '?lang=ru')) ?>">
|
||||
<link rel="alternate" hreflang="fr" href="<?php echo h($canonicalUrl . (str_contains($canonicalUrl, '?') ? '&lang=fr' : '?lang=fr')) ?>">
|
||||
<link rel="alternate" hreflang="x-default" href="<?php echo h($canonicalUrl) ?>">
|
||||
|
||||
<meta property="og:title" content="<?php echo h($seoTitle) ?>">
|
||||
<meta property="og:description" content="<?php echo h($seoDesc) ?>">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="<?php echo h($canonicalUrl) ?>">
|
||||
<meta property="og:image" content="https://uslugi.fr/public/uploads/img/logo.png">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<link href="/css/bootstrap.min.css" rel="stylesheet">
|
||||
@@ -296,8 +518,8 @@ else: ?>
|
||||
<div class="container">
|
||||
<div class="header-left">
|
||||
<div class="brand">
|
||||
<a href="index.php"><img src="/public/uploads/img/logo_u.png" alt="uslugi.fr" class="logo-mobile"></a>
|
||||
<a href="index.php"><img src="/public/uploads/img/logo.png" alt="uslugi.fr" class="logo-desktop"></a>
|
||||
<a href="/index.php"><img src="/public/uploads/img/logo_u.png" alt="uslugi.fr" class="logo-mobile"></a>
|
||||
<a href="/index.php"><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'; ?>
|
||||
@@ -330,6 +552,7 @@ else: ?>
|
||||
<div class="content">
|
||||
<div class="container">
|
||||
<h1 class="results-title"><?php echo h($h1) ?></h1>
|
||||
<p class="visually-hidden"><?php echo h($seoIntro) ?></p>
|
||||
</div>
|
||||
|
||||
<section class="cards-section">
|
||||
@@ -342,25 +565,25 @@ else: ?>
|
||||
?>
|
||||
<article class="pro-card">
|
||||
<div class="pro-photo">
|
||||
<a href="/detail_public.php?id=<?php echo (int) $p['id'] ?>">
|
||||
<img src="<?php echo h($img ?: '/public/uploads/img/user_placeholder.png') ?>" alt="Фото специалиста">
|
||||
</a>
|
||||
</div>
|
||||
<a href="/detail_public.php?id=<?php echo (int) $p['id'] ?>">
|
||||
<img src="<?php echo h($img ?: '/public/uploads/img/user_placeholder.png') ?>" alt="<?php echo h(trim(($p['prenom_affiche'] ?? '') . ' ' . ($p['nom_affiche'] ?? '') . ' — ' . ($p['specialite_affichee'] ?? '') . (! empty($p['ville_affichee']) ? ' — ' . $p['ville_affichee'] : ''))) ?>">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="pro-info">
|
||||
<h3 class="pro-name">
|
||||
<a href="/detail_public.php?id=<?php echo (int) $p['id'] ?>">
|
||||
<?php echo h(($p['prenom'] ?? '') . ' ' . ($p['nom'] ?? '')) ?>
|
||||
<?php echo h(($p['prenom_affiche'] ?? '') . ' ' . ($p['nom_affiche'] ?? '')) ?>
|
||||
</a>
|
||||
</h3>
|
||||
|
||||
<?php if (! empty($p['specialite'])): ?>
|
||||
<div class="pro-spec"><?php echo h($p['specialite']) ?></div>
|
||||
<?php if (! empty($p['specialite_affichee'])): ?>
|
||||
<div class="pro-spec"><?php echo h($p['specialite_affichee']) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (! empty($p['presentation'])): ?>
|
||||
<p class="pro-desc"><?php echo h(excerpt($p['presentation'], 360)) ?></p>
|
||||
<?php endif; ?>
|
||||
<?php if (! empty($p['presentation_affichee'])): ?>
|
||||
<p class="pro-desc"><?php echo h(excerpt($p['presentation_affichee'], 360)) ?></p>
|
||||
<?php endif; ?>
|
||||
|
||||
<a href="/detail_public.php?id=<?php echo (int) $p['id'] ?>">
|
||||
<?php echo h($translations['plus_details'] ?? 'Подробнее'); ?>
|
||||
|
||||
@@ -0,0 +1,516 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
|
||||
require_once __DIR__ . '/../includes/bootstrap_global.php';
|
||||
require_once APP_HTDOCS . '/includes/avatar_initiales.php';
|
||||
require_once __DIR__ . '/includes/search_helpers.php';
|
||||
|
||||
$SPEC_CANON = require __DIR__ . '/includes/spec_map.php';
|
||||
$canon = $SPEC_CANON[$lang] ?? [];
|
||||
|
||||
$H1_LABELS = [
|
||||
'ru' => [
|
||||
'avocats' => 'Адвокаты',
|
||||
'consultants' => 'Консультанты',
|
||||
'vrachi' => 'Врачи',
|
||||
'psychologues' => 'Психологи',
|
||||
'repetitori' => 'Преподаватели',
|
||||
'finance' => 'Бухгалтеры',
|
||||
'perevod' => 'Переводчики',
|
||||
'notarius' => 'Нотариусы',
|
||||
'gid' => 'Гиды',
|
||||
'rieltori' => 'Риелторы',
|
||||
'drugie' => 'Специалисты',
|
||||
],
|
||||
'fr' => [
|
||||
'avocats' => 'Avocats',
|
||||
'consultants' => 'Consultants',
|
||||
'vrachi' => 'Médecins',
|
||||
'psychologues' => 'Psychologues',
|
||||
'repetitori' => 'Professeurs',
|
||||
'finance' => 'Comptables',
|
||||
'perevod' => 'Traducteurs',
|
||||
'notarius' => 'Notaires',
|
||||
'gid' => 'Guides',
|
||||
'rieltori' => 'Agents immobiliers',
|
||||
'drugie' => 'Spécialistes',
|
||||
],
|
||||
];
|
||||
|
||||
// Helper detection video
|
||||
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"];
|
||||
}
|
||||
|
||||
// Direct file (mp4/webm/ogg) — no reliable thumb
|
||||
$path = parse_url($u, PHP_URL_PATH) ?? '';
|
||||
if (preg_match('~\.(mp4|webm|ogg)$~i', $path)) {
|
||||
return ['provider' => 'file', 'id' => null, 'thumb' => null];
|
||||
}
|
||||
|
||||
// Vimeo and others — fallback placeholder
|
||||
if (strpos(strtolower($u), 'vimeo.com') !== false) {
|
||||
return ['provider' => 'vimeo', 'id' => null, 'thumb' => null];
|
||||
}
|
||||
|
||||
return ['provider' => 'other', 'id' => null, 'thumb' => null];
|
||||
}
|
||||
|
||||
// Lecture et normalisation des filtres
|
||||
$specialiteParam = trim($_GET['specialite'] ?? '');
|
||||
$ville = trim($_GET['ville'] ?? '');
|
||||
|
||||
// inverse map: canon_value -> key (avocats, vrachi, ...)
|
||||
$canonToKey = [];
|
||||
foreach (($SPEC_CANON[$lang] ?? []) as $k => $v) {
|
||||
$canonToKey[mb_strtolower($v, 'UTF-8')] = $k;
|
||||
}
|
||||
|
||||
$inFrance = ($lang === 'fr') ? ' en France' : ' во Франции';
|
||||
$h1Default = $translations['res_poisk'] ?? 'Результаты поиска';
|
||||
$h1 = $h1Default;
|
||||
|
||||
// On normalise l'affichage du terme (pour l'utiliser plus tard si besoin)
|
||||
$sp = trim(str_replace(["\xC2\xA0"], ' ', $specialiteParam));
|
||||
$sp = preg_replace('/\s+/u', ' ', $sp);
|
||||
$sp_l = mb_strtolower($sp, 'UTF-8');
|
||||
|
||||
if ($specialiteParam === '__OTHERS__') {
|
||||
$label = $H1_LABELS[$lang]['drugie'] ?? $h1Default;
|
||||
$h1 = $label . $inFrance;
|
||||
} elseif ($sp !== '' && isset($canonToKey[$sp_l])) {
|
||||
$key = $canonToKey[$sp_l];
|
||||
$label = $H1_LABELS[$lang][$key] ?? ($translations[$key] ?? $sp);
|
||||
$h1 = $label . $inFrance;
|
||||
}
|
||||
// sinon: on laisse $h1Default, on décidera après la requête SQL (selon résultats)
|
||||
|
||||
// Cas spécial : "Другие специалисты" (chip "drugie")
|
||||
$onlyOthers = false;
|
||||
$specialite = $specialiteParam;
|
||||
|
||||
if ($specialiteParam === '__OTHERS__') {
|
||||
$onlyOthers = true;
|
||||
$specialite = '';
|
||||
}
|
||||
|
||||
// Pagination simple
|
||||
$perPage = 12;
|
||||
$page = max(1, (int) ($_GET['page'] ?? 1));
|
||||
$offset = ($page - 1) * $perPage;
|
||||
|
||||
// Construction SQL
|
||||
$where = [];
|
||||
$params = [];
|
||||
|
||||
/*==========================BLOCK WHERE AVEC ABONNEMENT VALIDE ==========================*/
|
||||
$where[] = "(date_expiration IS NOT NULL AND date_expiration >= DATE_SUB(NOW(), INTERVAL 14 DAY))";
|
||||
/*=======================================================================================*/
|
||||
|
||||
// 🔎 Unifie: le champ "specialite" peut contenir une spécialité OU un nom/prénom
|
||||
$term = trim((string) $specialite);
|
||||
$term_l = mb_strtolower($term, 'UTF-8');
|
||||
|
||||
// 1) Cas "Другие": tout sauf les grandes catégories
|
||||
if ($onlyOthers) {
|
||||
$mainSpecs = array_values($canon);
|
||||
if ($mainSpecs) {
|
||||
$ors = [];
|
||||
foreach ($mainSpecs as $i => $spc) {
|
||||
$key = ":cat{$i}";
|
||||
$ors[] = "LOWER(specialite) LIKE $key";
|
||||
$params[$key] = mb_strtolower($spc, 'UTF-8') . '%';
|
||||
}
|
||||
$where[] = '(specialite IS NULL OR specialite = "" OR NOT (' . implode(' OR ', $ors) . '))';
|
||||
}
|
||||
}
|
||||
// 2) Cas normal : recherche par spécialité / nom / prénom
|
||||
elseif ($term !== '') {
|
||||
$where[] =
|
||||
"("
|
||||
. "LOWER(specialite) LIKE :sp_like "
|
||||
. "OR LOWER(nom) LIKE :nm_prefix1 "
|
||||
. "OR LOWER(prenom) LIKE :nm_prefix2 "
|
||||
. "OR LOWER(CONCAT(prenom,' ',nom)) LIKE :nm_like_full1 "
|
||||
. "OR LOWER(CONCAT(nom,' ',prenom)) LIKE :nm_like_full2 "
|
||||
. ")";
|
||||
$params[':sp_like'] = '%' . $term_l . '%';
|
||||
$params[':nm_prefix1'] = $term_l . '%';
|
||||
$params[':nm_prefix2'] = $term_l . '%';
|
||||
$params[':nm_like_full1'] = '%' . $term_l . '%';
|
||||
$params[':nm_like_full2'] = '%' . $term_l . '%';
|
||||
}
|
||||
|
||||
// Filtre ville — support latin + cyrillique (alias + translit)
|
||||
if ($ville !== '') {
|
||||
$villeCands = city_query_candidates($ville);
|
||||
$villeCands[] = mb_strtolower($ville, 'UTF-8');
|
||||
$villeCands = array_values(array_unique($villeCands));
|
||||
|
||||
$villeOrs = [];
|
||||
foreach ($villeCands as $i => $cand) {
|
||||
$key = ":ville{$i}";
|
||||
$params[$key] = $cand . '%';
|
||||
$villeOrs[] = "ville LIKE {$key}";
|
||||
}
|
||||
if ($villeOrs) {
|
||||
$where[] = '(' . implode(' OR ', $villeOrs) . ')';
|
||||
}
|
||||
}
|
||||
|
||||
$whereSql = $where ? ('WHERE ' . implode(' AND ', $where)) : '';
|
||||
|
||||
// total
|
||||
$sqlCount = "SELECT COUNT(*) FROM praticien $whereSql";
|
||||
$stmt = $pdo->prepare($sqlCount);
|
||||
$stmt->execute($params);
|
||||
$total = (int) $stmt->fetchColumn();
|
||||
$pages = max(1, (int) ceil($total / $perPage));
|
||||
|
||||
// data
|
||||
$sql = "SELECT id, nom, prenom, specialite, ville, photo, presentation, video
|
||||
FROM praticien
|
||||
$whereSql";
|
||||
|
||||
if ($term !== '') {
|
||||
$sql .= " ORDER BY
|
||||
(LOWER(CONCAT(prenom,' ',nom)) = :nm_exact1
|
||||
OR LOWER(CONCAT(nom,' ',prenom)) = :nm_exact2) DESC,
|
||||
(LOWER(nom) = :nm_exact3 OR LOWER(prenom) = :nm_exact4) DESC,
|
||||
(LOWER(nom) LIKE :nm_prefix3 OR LOWER(prenom) LIKE :nm_prefix4) DESC,
|
||||
(LOWER(specialite) LIKE :sp_prefix1) DESC,
|
||||
nom ASC, prenom ASC";
|
||||
$params[':nm_exact1'] = $term_l;
|
||||
$params[':nm_exact2'] = $term_l;
|
||||
$params[':nm_exact3'] = $term_l;
|
||||
$params[':nm_exact4'] = $term_l;
|
||||
$params[':nm_prefix3'] = $term_l . '%';
|
||||
$params[':nm_prefix4'] = $term_l . '%';
|
||||
$params[':sp_prefix1'] = $term_l . '%';
|
||||
} else {
|
||||
$sql .= " ORDER BY updated_at DESC, id DESC";
|
||||
}
|
||||
|
||||
$sql .= " LIMIT :offset, :limit";
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
|
||||
// Lie tous les :villeN / :specialite
|
||||
foreach ($params as $k => $v) {
|
||||
$stmt->bindValue($k, $v, PDO::PARAM_STR);
|
||||
}
|
||||
$stmt->bindValue(':offset', (int) $offset, PDO::PARAM_INT);
|
||||
$stmt->bindValue(':limit', (int) $perPage, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// ✅ Si ce n'est pas une catégorie canon / pas __OTHERS__
|
||||
// et qu'on a des résultats => on affiche "Résultats pour : X"
|
||||
if ($h1 === $h1Default && $specialiteParam !== '__OTHERS__' && $sp !== '' && ! isset($canonToKey[$sp_l])) {
|
||||
if (! empty($rows)) {
|
||||
if ($lang === 'fr') {
|
||||
$h1 = ''; //'Résultats de recherche pour : ' . $sp;
|
||||
} else {
|
||||
$h1 = ''; //'Результаты поиска по: ' . $sp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// util
|
||||
function h($s): string
|
||||
{
|
||||
return htmlspecialchars((string) $s, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
function excerpt($text, $len = 160): string
|
||||
{
|
||||
$t = trim(strip_tags((string) $text));
|
||||
if (mb_strlen($t, 'UTF-8') <= $len) {
|
||||
return $t;
|
||||
}
|
||||
|
||||
return mb_substr($t, 0, $len, 'UTF-8') . '…';
|
||||
}
|
||||
|
||||
// pour liens de pagination
|
||||
$qParams = [];
|
||||
|
||||
if ($onlyOthers) {
|
||||
$qParams['specialite'] = '__OTHERS__';
|
||||
} elseif ($specialite !== '') {
|
||||
$qParams['specialite'] = $specialite;
|
||||
}
|
||||
|
||||
if ($ville !== '') {
|
||||
$qParams['ville'] = $ville;
|
||||
}
|
||||
|
||||
$baseQuery = http_build_query($qParams);
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title><?php echo htmlspecialchars($translations['seo_result_search_title'] ?? '', ENT_QUOTES, 'UTF-8') ?></title>
|
||||
<meta name="description" content="<?php echo htmlspecialchars($translations['seo_result_search_desc'] ?? '', ENT_QUOTES, 'UTF-8') ?>">
|
||||
<meta name="keywords" content="<?php echo htmlspecialchars($translations['seo_result_search_keywords'] ?? '', ENT_QUOTES, 'UTF-8') ?>">
|
||||
<meta name="author" content="Uslugi.fr">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<link href="/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
<link rel="stylesheet" href="<?php echo asset('/includes/site_style.css') ?>">
|
||||
<link rel="stylesheet" href="<?php echo asset('/includes/custom_style.css') ?>">
|
||||
<link rel="stylesheet" href="<?php echo asset('/includes/mobil_style.css') ?>">
|
||||
|
||||
<?php
|
||||
$trackingFile = __DIR__ . '/includes/tracking.php';
|
||||
if (is_file($trackingFile)) {
|
||||
include_once $trackingFile;
|
||||
}
|
||||
?>
|
||||
</head>
|
||||
|
||||
<body class="layout">
|
||||
<?php if (! empty($logged)):
|
||||
include_header_clean(APP_HTDOCS . '/public/includes/header.php');
|
||||
else: ?>
|
||||
<header class="site-header">
|
||||
<div class="container">
|
||||
<div class="header-left">
|
||||
<div class="brand">
|
||||
<a href="index.php"><img src="/public/uploads/img/logo_u.png" alt="uslugi.fr" class="logo-mobile"></a>
|
||||
<a href="index.php"><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>
|
||||
|
||||
<nav class="header-actions">
|
||||
<a class="btn-login" href="/connexion.php">
|
||||
<span class="d-sm-none"><?php echo h($translations['btn_se_connecter'] ?? '') ?></span>
|
||||
<span class="d-none d-sm-inline"><?php echo h($translations['ja_spec'] ?? '') ?></span>
|
||||
</a>
|
||||
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-logo rounded-circle d-inline-flex align-items-center justify-content-center question-btn"
|
||||
type="button" data-bs-toggle="dropdown" data-bs-auto-close="true"
|
||||
aria-expanded="false" aria-label="Помощь">?</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end shadow help-menu menu-panel">
|
||||
<li><a href="/how-it-work.php"><?php echo h($translations['kak_rabotaet'] ?? '') ?></a></li>
|
||||
<li><a href="/about.php"><?php echo h($translations['pro_nas'] ?? '') ?></a></li>
|
||||
<li><a href="/faq.php"><?php echo h($translations['faq'] ?? '') ?></a></li>
|
||||
<li><a href="/contact.php"><?php echo h($translations['nam_pisat'] ?? '') ?></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="main1">
|
||||
<div class="content">
|
||||
<div class="container">
|
||||
<h1 class="results-title"><?php echo h($h1) ?></h1>
|
||||
</div>
|
||||
|
||||
<section class="cards-section">
|
||||
<div class="container pro-list">
|
||||
<?php if ($rows): ?>
|
||||
<?php foreach ($rows as $p):
|
||||
$img = ! empty($p['photo'])
|
||||
? "/public/uploads/" . ltrim((string) $p['photo'], '/')
|
||||
: "/public/uploads/img/user_placeholder.png";
|
||||
?>
|
||||
<article class="pro-card">
|
||||
<div class="pro-photo">
|
||||
<a href="/detail_public.php?id=<?php echo (int) $p['id'] ?>">
|
||||
<img src="<?php echo h($img ?: '/public/uploads/img/user_placeholder.png') ?>" alt="Фото специалиста">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="pro-info">
|
||||
<h3 class="pro-name">
|
||||
<a href="/detail_public.php?id=<?php echo (int) $p['id'] ?>">
|
||||
<?php echo h(($p['prenom'] ?? '') . ' ' . ($p['nom'] ?? '')) ?>
|
||||
</a>
|
||||
</h3>
|
||||
|
||||
<?php if (! empty($p['specialite'])): ?>
|
||||
<div class="pro-spec"><?php echo h($p['specialite']) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (! empty($p['presentation'])): ?>
|
||||
<p class="pro-desc"><?php echo h(excerpt($p['presentation'], 360)) ?></p>
|
||||
<?php endif; ?>
|
||||
|
||||
<a href="/detail_public.php?id=<?php echo (int) $p['id'] ?>">
|
||||
<?php echo h($translations['plus_details'] ?? 'Подробнее'); ?>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<?php if (! empty($p['video'])): ?>
|
||||
<div class="pro-video-col">
|
||||
<?php
|
||||
$vinfo = video_thumb_info($p['video']);
|
||||
$alt = htmlspecialchars($translations['video_alt'] ?? 'Vidéo', ENT_QUOTES, 'UTF-8');
|
||||
$thumb = $vinfo['thumb'] ?? null;
|
||||
$prov = $vinfo['provider'] ?? 'other';
|
||||
$vid = $vinfo['id'] ?? null;
|
||||
$raw = trim((string) ($p['video'] ?? ''));
|
||||
?>
|
||||
|
||||
<?php if (in_array($prov, ['youtube', 'dailymotion'], true) && $vid): ?>
|
||||
<a class="pro-video" href="#"
|
||||
data-provider="<?php echo htmlspecialchars($prov, ENT_QUOTES, 'UTF-8') ?>"
|
||||
data-id="<?php echo htmlspecialchars($vid, ENT_QUOTES, 'UTF-8') ?>"
|
||||
aria-label="<?php echo $alt ?>">
|
||||
<img src="<?php echo htmlspecialchars($thumb ?: '/public/uploads/img/video_placeholder.jpg', ENT_QUOTES, 'UTF-8') ?>"
|
||||
alt="<?php echo $alt ?>">
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<a class="pro-video" href="<?php echo htmlspecialchars($raw, ENT_QUOTES, 'UTF-8') ?>" target="_blank" rel="noopener">
|
||||
<img src="<?php echo htmlspecialchars($thumb ?: '/public/uploads/img/video_placeholder.jpg', ENT_QUOTES, 'UTF-8') ?>"
|
||||
alt="<?php echo $alt ?>">
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="pro-cta-col">
|
||||
<a class="pro-cta" href="public/client/reserver_rdv.php?praticien_id=<?php echo (int) $p['id'] ?>">
|
||||
<?php echo h($translations['den'] ?? 'Выберите день') ?>
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<div class="no-results"><?php echo h($translations['probuite'] ?? 'Нет результатов') ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if ($pages > 1): ?>
|
||||
<?php
|
||||
$qBase = $baseQuery ? ($baseQuery . '&') : '';
|
||||
$prevPage = max(1, $page - 1);
|
||||
$nextPage = min($pages, $page + 1);
|
||||
?>
|
||||
<nav class="pager" aria-label="Pagination">
|
||||
<a class="pager-arrow <?php echo $page <= 1 ? 'is-disabled' : '' ?>"
|
||||
href="<?php echo h('/result_search.php?' . $qBase . 'page=' . $prevPage) ?>"
|
||||
aria-label="Page précédente">««</a>
|
||||
|
||||
<?php
|
||||
$start = max(1, $page - 2);
|
||||
$end = min($pages, $page + 2);
|
||||
for ($i = $start; $i <= $end; $i++):
|
||||
$href = '/result_search.php?' . $qBase . 'page=' . $i;
|
||||
?>
|
||||
<a class="pager-page <?php echo $i === $page ? 'is-active' : '' ?>"
|
||||
href="<?php echo h($href) ?>"><?php echo (int) $i ?></a>
|
||||
<?php endfor; ?>
|
||||
|
||||
<a class="pager-arrow <?php echo $page >= $pages ? 'is-disabled' : '' ?>"
|
||||
href="<?php echo h('/result_search.php?' . $qBase . 'page=' . $nextPage) ?>"
|
||||
aria-label="Page suivante">»»</a>
|
||||
</nav>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal vidéo -->
|
||||
<div id="lightboxVideo" class="vmodal" aria-hidden="true">
|
||||
<div class="vmodal__backdrop" data-close></div>
|
||||
<div class="vmodal__dialog" role="dialog" aria-modal="true" aria-label="Vidéo">
|
||||
<div class="vmodal__header">
|
||||
<h5 class="vmodal__title">Vidéo</h5>
|
||||
<button class="vmodal__close" type="button" title="Fermer" aria-label="Fermer" data-close>×</button>
|
||||
</div>
|
||||
|
||||
<div class="vmodal__body">
|
||||
<div class="vmodal__ratio">
|
||||
<iframe id="vPlayer" src="" title="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="vmodal__footer">
|
||||
<button class="btn btn-secondary-like" type="button" data-close>Fermer</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p><?php echo $translations['copyr'] ?></p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
const modal = document.getElementById('lightboxVideo');
|
||||
const frame = document.getElementById('vPlayer');
|
||||
|
||||
function buildSrc(provider, id){
|
||||
if (!provider || !id) return '';
|
||||
if (provider === 'youtube') return 'https://www.youtube-nocookie.com/embed/' + id + '?autoplay=1&rel=0&modestbranding=1';
|
||||
if (provider === 'dailymotion') return 'https://www.dailymotion.com/embed/video/' + id + '?autoplay=1&ui-start-screen-info=false';
|
||||
return '';
|
||||
}
|
||||
|
||||
function openModal(src){
|
||||
if (!src) return;
|
||||
frame.src = src;
|
||||
modal.setAttribute('aria-hidden','false');
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
function closeModal(){
|
||||
frame.src = '';
|
||||
modal.setAttribute('aria-hidden','true');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
document.addEventListener('click', function(e){
|
||||
const a = e.target.closest('a.pro-video');
|
||||
if (!a) return;
|
||||
|
||||
const provider = a.getAttribute('data-provider');
|
||||
const id = a.getAttribute('data-id');
|
||||
|
||||
if (provider && id){
|
||||
e.preventDefault();
|
||||
const src = buildSrc(provider, id);
|
||||
openModal(src);
|
||||
}
|
||||
});
|
||||
|
||||
modal.addEventListener('click', (e)=>{ if (e.target.hasAttribute('data-close')) closeModal(); });
|
||||
window.addEventListener('keydown', (e)=>{ if (e.key === 'Escape' && modal.getAttribute('aria-hidden') === 'false') closeModal(); });
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script src="/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://uslugi.fr/sitemap.xml
|
||||
Sitemap: https://uslugi.fr/sitemap.php
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
// Mapping inverse
|
||||
$UGLY_TO_PRETTY = [
|
||||
//racine
|
||||
'/index.php' => '/',
|
||||
'/result_search.php' => '/recherche',
|
||||
'/detail_public.php' => '/specialistes',
|
||||
'/connexion.php' => '/login',
|
||||
'/choix_inscr.php' => '/choix-inscription',
|
||||
'/inscription_client.php' => '/inscription-client',
|
||||
'/inscription_praticien.php' => '/inscription-praticien',
|
||||
'/mdp_oublie.php' => '/mot-de-passe-oublie',
|
||||
'/avis_public.php' => '/avis',
|
||||
//dossier client
|
||||
'/public/client/informations.php' => '/profil',
|
||||
'/public/client/modifier.php' => '/modifier-profil',
|
||||
'/public/client/compte.php' => '/mon-compte',
|
||||
'/public/client/list_praticien.php' => '/trouver-specialistes',
|
||||
'/public/client/mes_favori.php' => '/mes-preferences',
|
||||
'/public/client/detail_praticien.php' => '/specialiste',
|
||||
'/public/client/avis.php' => '/donner-avis',
|
||||
'/public/client/reserver_rdv.php' => '/reservation',
|
||||
'/public/client/mes_rendez_vous.php' => '/mes-rendez-vous',
|
||||
|
||||
//Dossier praticien
|
||||
'/public/praticien/informations.php' => '/profil-pro',
|
||||
'/public/praticien/modifier.php' => '/modifier-profil-pro',
|
||||
'/public/praticien/compte.php' => '/mon-compte-pro',
|
||||
'/public/praticien/disponibilites.php' => '/mes-creneaux',
|
||||
'/public/praticien/disponibilites_date.php' => '/mes-creneaux-date',
|
||||
'/public/praticien/rendez_vous.php' => '/mes-consultations',
|
||||
'/public/praticien/rendez_vous_du_jour.php' => '/consultations-du-jour',
|
||||
'/public/praticien/agenda.php' => '/agenda',
|
||||
'/public/praticien/synchronisation.php' => '/connecter-agenda',
|
||||
'/public/praticien/resiliation.php' => '/abonnement',
|
||||
'/public/praticien/factures.php' => '/mes-factures',
|
||||
|
||||
//Dossier messages
|
||||
|
||||
];
|
||||
$PRETTY_TO_UGLY = array_flip($UGLY_TO_PRETTY);
|
||||
|
||||
// Quel slug demandé ? ex: page-1.htm
|
||||
$slug = $_GET['slug'] ?? '';
|
||||
$slug = '/' . ltrim($slug, '/');
|
||||
|
||||
if (!isset($PRETTY_TO_UGLY[$slug])) {
|
||||
http_response_code(404);
|
||||
echo 'Page non trouvée';
|
||||
exit;
|
||||
}
|
||||
|
||||
$targetRelative = $PRETTY_TO_UGLY[$slug]; // ex: /public/client/informations.php
|
||||
$targetAbsolute = $_SERVER['DOCUMENT_ROOT'] . $targetRelative;
|
||||
|
||||
if (!is_file($targetAbsolute)) {
|
||||
http_response_code(404);
|
||||
echo 'Cible introuvable';
|
||||
exit;
|
||||
}
|
||||
|
||||
// Marquer qu'on passe par router pour éviter boucle
|
||||
define('ROUTED_PRETTY', true);
|
||||
|
||||
// Aller dans le bon dossier
|
||||
chdir(dirname($targetAbsolute));
|
||||
|
||||
// Inclure la vraie page
|
||||
require $targetAbsolute;
|
||||
+106
-34
@@ -1,28 +1,30 @@
|
||||
<?php
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/search_helpers.php';
|
||||
|
||||
// Mapping inverse
|
||||
$UGLY_TO_PRETTY = [
|
||||
//racine
|
||||
'/index.php' => '/',
|
||||
'/result_search.php' => '/recherche',
|
||||
'/detail_public.php' => '/specialistes',
|
||||
'/connexion.php' => '/login',
|
||||
'/choix_inscr.php' => '/choix-inscription',
|
||||
'/inscription_client.php' => '/inscription-client',
|
||||
'/inscription_praticien.php' => '/inscription-praticien',
|
||||
'/mdp_oublie.php' => '/mot-de-passe-oublie',
|
||||
'/avis_public.php' => '/avis',
|
||||
//dossier client
|
||||
'/public/client/informations.php' => '/profil',
|
||||
'/public/client/modifier.php' => '/modifier-profil',
|
||||
'/public/client/compte.php' => '/mon-compte',
|
||||
'/public/client/list_praticien.php' => '/trouver-specialistes',
|
||||
'/public/client/mes_favori.php' => '/mes-preferences',
|
||||
'/public/client/detail_praticien.php' => '/specialiste',
|
||||
'/public/client/avis.php' => '/donner-avis',
|
||||
'/public/client/reserver_rdv.php' => '/reservation',
|
||||
'/public/client/mes_rendez_vous.php' => '/mes-rendez-vous',
|
||||
// racine
|
||||
'/index.php' => '/',
|
||||
'/detail_public.php' => '/specialistes',
|
||||
'/connexion.php' => '/login',
|
||||
'/choix_inscr.php' => '/choix-inscription',
|
||||
'/inscription_client.php' => '/inscription-client',
|
||||
'/inscription_praticien.php' => '/inscription-praticien',
|
||||
'/mdp_oublie.php' => '/mot-de-passe-oublie',
|
||||
'/avis_public.php' => '/avis',
|
||||
|
||||
//Dossier praticien
|
||||
// dossier client
|
||||
'/public/client/informations.php' => '/profil',
|
||||
'/public/client/modifier.php' => '/modifier-profil',
|
||||
'/public/client/compte.php' => '/mon-compte',
|
||||
'/public/client/list_praticien.php' => '/trouver-specialistes',
|
||||
'/public/client/mes_favori.php' => '/mes-preferences',
|
||||
'/public/client/detail_praticien.php' => '/specialiste',
|
||||
'/public/client/avis.php' => '/donner-avis',
|
||||
'/public/client/reserver_rdv.php' => '/reservation',
|
||||
'/public/client/mes_rendez_vous.php' => '/mes-rendez-vous',
|
||||
|
||||
// dossier praticien
|
||||
'/public/praticien/informations.php' => '/profil-pro',
|
||||
'/public/praticien/modifier.php' => '/modifier-profil-pro',
|
||||
'/public/praticien/compte.php' => '/mon-compte-pro',
|
||||
@@ -34,36 +36,106 @@ $UGLY_TO_PRETTY = [
|
||||
'/public/praticien/synchronisation.php' => '/connecter-agenda',
|
||||
'/public/praticien/resiliation.php' => '/abonnement',
|
||||
'/public/praticien/factures.php' => '/mes-factures',
|
||||
|
||||
//Dossier messages
|
||||
|
||||
];
|
||||
|
||||
$PRETTY_TO_UGLY = array_flip($UGLY_TO_PRETTY);
|
||||
|
||||
// Quel slug demandé ? ex: page-1.htm
|
||||
// Quel slug demandé ?
|
||||
$slug = $_GET['slug'] ?? '';
|
||||
$slug = '/' . ltrim($slug, '/');
|
||||
$slug = '/' . trim($slug, '/');
|
||||
|
||||
if (!isset($PRETTY_TO_UGLY[$slug])) {
|
||||
// =========================
|
||||
// Routes dynamiques recherche
|
||||
// =========================
|
||||
|
||||
// /recherche
|
||||
if ($slug === '/recherche') {
|
||||
$targetRelative = '/result_search.php';
|
||||
$targetAbsolute = $_SERVER['DOCUMENT_ROOT'] . $targetRelative;
|
||||
|
||||
define('ROUTED_PRETTY', true);
|
||||
chdir(dirname($targetAbsolute));
|
||||
require $targetAbsolute;
|
||||
exit;
|
||||
}
|
||||
// /recherche/ville/{ville}
|
||||
if (preg_match('#^/recherche/ville/([^/]+)$#', $slug, $m)) {
|
||||
$_GET['ville'] = seo_unslug(rawurldecode($m[1]));
|
||||
|
||||
$targetRelative = '/result_search.php';
|
||||
$targetAbsolute = $_SERVER['DOCUMENT_ROOT'] . $targetRelative;
|
||||
|
||||
define('ROUTED_PRETTY', true);
|
||||
chdir(dirname($targetAbsolute));
|
||||
require $targetAbsolute;
|
||||
exit;
|
||||
}
|
||||
// /recherche/{specialite}
|
||||
if (preg_match('#^/recherche/([^/]+)$#', $slug, $m)) {
|
||||
$_GET['specialite'] = seo_unslug(rawurldecode($m[1]));
|
||||
|
||||
$targetRelative = '/result_search.php';
|
||||
$targetAbsolute = $_SERVER['DOCUMENT_ROOT'] . $targetRelative;
|
||||
|
||||
define('ROUTED_PRETTY', true);
|
||||
chdir(dirname($targetAbsolute));
|
||||
require $targetAbsolute;
|
||||
exit;
|
||||
}
|
||||
|
||||
// /recherche/{specialite}/{ville}
|
||||
if (preg_match('#^/recherche/([^/]+)/([^/]+)$#', $slug, $m)) {
|
||||
$_GET['specialite'] = seo_unslug(rawurldecode($m[1]));
|
||||
$_GET['ville'] = seo_unslug(rawurldecode($m[2]));
|
||||
|
||||
$targetRelative = '/result_search.php';
|
||||
$targetAbsolute = $_SERVER['DOCUMENT_ROOT'] . $targetRelative;
|
||||
|
||||
define('ROUTED_PRETTY', true);
|
||||
chdir(dirname($targetAbsolute));
|
||||
require $targetAbsolute;
|
||||
exit;
|
||||
}
|
||||
// /specialistes/{specialite}/{ville}/{nom-id}
|
||||
if (preg_match('#^/specialistes/([^/]+)/([^/]+)/([^/]+)-([0-9]+)$#', $slug, $m)) {
|
||||
$_GET['id'] = (int) $m[4];
|
||||
|
||||
$targetRelative = '/detail_public.php';
|
||||
$targetAbsolute = $_SERVER['DOCUMENT_ROOT'] . $targetRelative;
|
||||
|
||||
define('ROUTED_PRETTY', true);
|
||||
chdir(dirname($targetAbsolute));
|
||||
require $targetAbsolute;
|
||||
exit;
|
||||
}
|
||||
// /sitemap.xml
|
||||
if ($slug === '/sitemap.xml') {
|
||||
$targetRelative = '/sitemap_xml.php';
|
||||
$targetAbsolute = $_SERVER['DOCUMENT_ROOT'] . $targetRelative;
|
||||
|
||||
define('ROUTED_PRETTY', true);
|
||||
chdir(dirname($targetAbsolute));
|
||||
require $targetAbsolute;
|
||||
exit;
|
||||
}
|
||||
// =========================
|
||||
// Routes fixes existantes
|
||||
// =========================
|
||||
if (! isset($PRETTY_TO_UGLY[$slug])) {
|
||||
http_response_code(404);
|
||||
echo 'Page non trouvée';
|
||||
exit;
|
||||
}
|
||||
|
||||
$targetRelative = $PRETTY_TO_UGLY[$slug]; // ex: /public/client/informations.php
|
||||
$targetRelative = $PRETTY_TO_UGLY[$slug];
|
||||
$targetAbsolute = $_SERVER['DOCUMENT_ROOT'] . $targetRelative;
|
||||
|
||||
if (!is_file($targetAbsolute)) {
|
||||
if (! is_file($targetAbsolute)) {
|
||||
http_response_code(404);
|
||||
echo 'Cible introuvable';
|
||||
exit;
|
||||
}
|
||||
|
||||
// Marquer qu'on passe par router pour éviter boucle
|
||||
define('ROUTED_PRETTY', true);
|
||||
|
||||
// Aller dans le bon dossier
|
||||
chdir(dirname($targetAbsolute));
|
||||
|
||||
// Inclure la vraie page
|
||||
require $targetAbsolute;
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
|
||||
require_once __DIR__ . '/../includes/bootstrap_global.php';
|
||||
require_once __DIR__ . '/includes/search_helpers.php';
|
||||
|
||||
header('Content-Type: application/xml; charset=UTF-8');
|
||||
|
||||
function xml_escape(string $s): string
|
||||
{
|
||||
return htmlspecialchars($s, ENT_QUOTES | ENT_XML1, 'UTF-8');
|
||||
}
|
||||
|
||||
$base = 'https://uslugi.fr';
|
||||
|
||||
// =========================
|
||||
// Pages fixes utiles
|
||||
// =========================
|
||||
$urls = [
|
||||
[
|
||||
'loc' => $base . '/',
|
||||
'changefreq' => 'weekly',
|
||||
'priority' => '1.0',
|
||||
'lastmod' => date('Y-m-d'),
|
||||
],
|
||||
[
|
||||
'loc' => $base . '/faq.php',
|
||||
'changefreq' => 'weekly',
|
||||
'priority' => '0.8',
|
||||
'lastmod' => date('Y-m-d'),
|
||||
],
|
||||
[
|
||||
'loc' => $base . '/about.php',
|
||||
'changefreq' => 'monthly',
|
||||
'priority' => '0.6',
|
||||
'lastmod' => date('Y-m-d'),
|
||||
],
|
||||
[
|
||||
'loc' => $base . '/how-it-work.php',
|
||||
'changefreq' => 'monthly',
|
||||
'priority' => '0.6',
|
||||
'lastmod' => date('Y-m-d'),
|
||||
],
|
||||
[
|
||||
'loc' => $base . '/contact.php',
|
||||
'changefreq' => 'monthly',
|
||||
'priority' => '0.6',
|
||||
'lastmod' => date('Y-m-d'),
|
||||
],
|
||||
];
|
||||
|
||||
// =========================
|
||||
// Pages catégories SEO
|
||||
// =========================
|
||||
$categorySlugs = [
|
||||
'avocat',
|
||||
'consultant',
|
||||
'medecin',
|
||||
'psychologue',
|
||||
'professeur',
|
||||
'comptable',
|
||||
'traducteur',
|
||||
'notaire',
|
||||
'guide',
|
||||
'agent-immobilier',
|
||||
'autres-specialistes',
|
||||
];
|
||||
|
||||
foreach ($categorySlugs as $slug) {
|
||||
$urls[] = [
|
||||
'loc' => $base . '/recherche/' . $slug . '/',
|
||||
'changefreq' => 'daily',
|
||||
'priority' => '0.9',
|
||||
'lastmod' => date('Y-m-d'),
|
||||
];
|
||||
}
|
||||
|
||||
// =========================
|
||||
// Profils spécialistes actifs
|
||||
// =========================
|
||||
$sql = "
|
||||
SELECT id, prenom, nom, ville, specialite, updated_at
|
||||
FROM praticien
|
||||
WHERE date_expiration IS NOT NULL
|
||||
AND date_expiration >= DATE_SUB(NOW(), INTERVAL 14 DAY)
|
||||
";
|
||||
$stmt = $pdo->query($sql);
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
foreach ($rows as $p) {
|
||||
$specSlug = 'specialiste';
|
||||
|
||||
if (! empty($p['specialite'])) {
|
||||
$specDb = mb_strtolower(trim((string) $p['specialite']), 'UTF-8');
|
||||
|
||||
foreach (seo_spec_map() as $slug => $cfg) {
|
||||
$dbVal = mb_strtolower(trim((string) ($cfg['db'] ?? '')), 'UTF-8');
|
||||
|
||||
if ($dbVal === $specDb || ($dbVal !== '' && mb_stripos($specDb, $dbVal, 0, 'UTF-8') === 0)) {
|
||||
$specSlug = $slug;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$citySlug = ! empty($p['ville']) ? seo_city_slug_from_input((string) $p['ville']) : 'france';
|
||||
|
||||
$nameSlug = seo_slug(trim((string) ($p['prenom'] ?? '') . ' ' . (string) ($p['nom'] ?? '')));
|
||||
if ($nameSlug === '') {
|
||||
$nameSlug = 'profil';
|
||||
}
|
||||
|
||||
$loc = $base . '/specialistes/'
|
||||
. rawurlencode($specSlug) . '/'
|
||||
. rawurlencode($citySlug) . '/'
|
||||
. rawurlencode($nameSlug . '-' . (int) $p['id']) . '/';
|
||||
|
||||
$lastmod = date('Y-m-d');
|
||||
if (! empty($p['updated_at'])) {
|
||||
$ts = strtotime((string) $p['updated_at']);
|
||||
if ($ts) {
|
||||
$lastmod = date('Y-m-d', $ts);
|
||||
}
|
||||
}
|
||||
|
||||
$urls[] = [
|
||||
'loc' => $loc,
|
||||
'changefreq' => 'weekly',
|
||||
'priority' => '0.8',
|
||||
'lastmod' => $lastmod,
|
||||
];
|
||||
}
|
||||
|
||||
// =========================
|
||||
// Sortie XML
|
||||
// =========================
|
||||
echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
|
||||
echo '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . "\n";
|
||||
|
||||
foreach ($urls as $url) {
|
||||
echo " <url>\n";
|
||||
echo ' <loc>' . xml_escape($url['loc']) . "</loc>\n";
|
||||
|
||||
if (! empty($url['lastmod'])) {
|
||||
echo ' <lastmod>' . xml_escape($url['lastmod']) . "</lastmod>\n";
|
||||
}
|
||||
|
||||
echo ' <changefreq>' . xml_escape($url['changefreq']) . "</changefreq>\n";
|
||||
echo ' <priority>' . xml_escape($url['priority']) . "</priority>\n";
|
||||
echo " </url>\n";
|
||||
}
|
||||
|
||||
echo '</urlset>';
|
||||
+7
-7
@@ -7,11 +7,11 @@ if (isset($_SERVER['SCRIPT_FILENAME']) && realpath(__FILE__) === realpath($_SERV
|
||||
|
||||
// Secrets par variables d'environnement (idéal en production)
|
||||
return [
|
||||
// Récupération depuis l'ENV si dispo, sinon fallback
|
||||
'phone_number_id' => getenv('WHATSAPP_PHONE_NUMBER_ID') ?: '720141641193323',
|
||||
'access_token' => getenv('WHATSAPP_TOKEN') ?: 'EAAYZApQ01JR8BPVMSMYVAYHzg6WHfFJW5Q9fyphrxvDl32hIq4X169LujTe0ffmlscl5t0vEFuYJrjNUCWFBHYOUGRJU5UT4QQYJb1TwqFH1Pn9ZBu9UeAi0cp6vUKmKNV40B3JlN1bhE2u0czZBX8dXko3Oh97dCIMijcY4Ib88aZCMUBxjBzsSQuk7Rl52QwZDZD',
|
||||
// Langue par défaut de tes templates
|
||||
'default_lang' => 'ru',
|
||||
// Timeout réseau cURL
|
||||
'timeout' => 20,
|
||||
// Récupération depuis l'ENV si dispo, sinon fallback
|
||||
'phone_number_id' => getenv('WHATSAPP_PHONE_NUMBER_ID') ?: '',
|
||||
'access_token' => getenv('WHATSAPP_TOKEN') ?: '',
|
||||
// Langue par défaut de tes templates
|
||||
'default_lang' => 'ru',
|
||||
// Timeout réseau cURL
|
||||
'timeout' => 20,
|
||||
];
|
||||
@@ -1,12 +1,12 @@
|
||||
<?php
|
||||
if (!defined('APP_BOOT')) { http_response_code(403); exit('Forbidden'); }
|
||||
if (! defined('APP_BOOT')) {http_response_code(403);exit('Forbidden');}
|
||||
// /sms/config.sample.php
|
||||
return [
|
||||
// Récupération depuis l'ENV si dispo, sinon fallback
|
||||
'phone_number_id' => getenv('WHATSAPP_PHONE_NUMBER_ID') ?: '720141641193323',
|
||||
'access_token' => getenv('WHATSAPP_TOKEN') ?: 'EAAYZApQ01JR8BPVMSMYVAYHzg6WHfFJW5Q9fyphrxvDl32hIq4X169LujTe0ffmlscl5t0vEFuYJrjNUCWFBHYOUGRJU5UT4QQYJb1TwqFH1Pn9ZBu9UeAi0cp6vUKmKNV40B3JlN1bhE2u0czZBX8dXko3Oh97dCIMijcY4Ib88aZCMUBxjBzsSQuk7Rl52QwZDZD',
|
||||
// Langue par défaut de tes templates
|
||||
'default_lang' => 'ru',
|
||||
// Timeout réseau cURL
|
||||
'timeout' => 20,
|
||||
];
|
||||
// Récupération depuis l'ENV si dispo, sinon fallback
|
||||
'phone_number_id' => getenv('WHATSAPP_PHONE_NUMBER_ID') ?: '',
|
||||
'access_token' => getenv('WHATSAPP_TOKEN') ?: '',
|
||||
// Langue par défaut de tes templates
|
||||
'default_lang' => 'ru',
|
||||
// Timeout réseau cURL
|
||||
'timeout' => 20,
|
||||
];
|
||||
@@ -0,0 +1,265 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
$form_index_status = null;
|
||||
$form_index_message = '';
|
||||
$form_index_errors = [];
|
||||
$form_index_old = [
|
||||
'name' => '',
|
||||
'category' => '',
|
||||
'situation' => '',
|
||||
'contact_type' => 'email',
|
||||
'email' => '',
|
||||
'wa' => '',
|
||||
];
|
||||
|
||||
if (! function_exists('t')) {
|
||||
function t(array $translations, string $key, string $fallback = ''): string
|
||||
{
|
||||
$val = $translations[$key] ?? '';
|
||||
return $val !== '' ? (string) $val : $fallback;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Flash message après redirection
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
if (isset($_SESSION['form_index_flash']) && is_array($_SESSION['form_index_flash'])) {
|
||||
$flash = $_SESSION['form_index_flash'];
|
||||
|
||||
$form_index_status = $flash['status'] ?? null;
|
||||
$form_index_message = $flash['message'] ?? '';
|
||||
$form_index_errors = $flash['errors'] ?? [];
|
||||
$form_index_old = $flash['old'] ?? $form_index_old;
|
||||
|
||||
unset($_SESSION['form_index_flash']);
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Si ce n'est pas le bon POST, on ne fait rien
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || ! isset($_POST['form_index'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Charger le mail helper
|
||||
|--------------------------------------------------------------------------
|
||||
| Adapte le chemin si ton fichier s'appelle autrement
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
require_once __DIR__ . '/includes/mailer.php';
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Récupération des données
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
$form_index_old = [
|
||||
'name' => trim((string) ($_POST['name'] ?? '')),
|
||||
'category' => trim((string) ($_POST['category'] ?? '')),
|
||||
'situation' => trim((string) ($_POST['situation'] ?? '')),
|
||||
'contact_type' => ($_POST['contact_type'] ?? 'email') === 'wa' ? 'wa' : 'email',
|
||||
'email' => trim((string) ($_POST['email'] ?? '')),
|
||||
'wa' => trim((string) ($_POST['wa'] ?? '')),
|
||||
];
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Honeypot anti-spam
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
$honeypot = trim((string) ($_POST['company'] ?? ''));
|
||||
if ($honeypot !== '') {
|
||||
$_SESSION['form_index_flash'] = [
|
||||
'status' => 'success',
|
||||
'message' => t(
|
||||
$translations,
|
||||
'form_index_success_text',
|
||||
'Мы изучим вашу ситуацию и свяжемся с вами в ближайшее время, чтобы предложить подходящего специалиста.'
|
||||
),
|
||||
'old' => [
|
||||
'name' => '',
|
||||
'category' => '',
|
||||
'situation' => '',
|
||||
'contact_type' => 'email',
|
||||
'email' => '',
|
||||
'wa' => '',
|
||||
],
|
||||
'errors' => [],
|
||||
];
|
||||
|
||||
header('Location: ' . strtok($_SERVER['REQUEST_URI'], '?') . '#concierge-form');
|
||||
exit;
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Validation
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
if ($form_index_old['name'] === '') {
|
||||
$form_index_errors['name'] = true;
|
||||
}
|
||||
|
||||
$allowedCategories = [
|
||||
'lawyer',
|
||||
'notary',
|
||||
'real_estate',
|
||||
'psychologist',
|
||||
'dentist_ortho',
|
||||
'financial_advisor',
|
||||
'translator',
|
||||
'residence_consultant',
|
||||
'repair',
|
||||
'funeral',
|
||||
'other',
|
||||
];
|
||||
|
||||
if ($form_index_old['category'] === '' || ! in_array($form_index_old['category'], $allowedCategories, true)) {
|
||||
$form_index_errors['category'] = true;
|
||||
}
|
||||
|
||||
if ($form_index_old['situation'] === '') {
|
||||
$form_index_errors['situation'] = true;
|
||||
}
|
||||
|
||||
if ($form_index_old['contact_type'] === 'email') {
|
||||
if ($form_index_old['email'] === '' || ! filter_var($form_index_old['email'], FILTER_VALIDATE_EMAIL)) {
|
||||
$form_index_errors['email'] = true;
|
||||
}
|
||||
} else {
|
||||
if ($form_index_old['wa'] === '') {
|
||||
$form_index_errors['wa'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($form_index_errors)) {
|
||||
$_SESSION['form_index_flash'] = [
|
||||
'status' => 'error',
|
||||
'message' => t(
|
||||
$translations,
|
||||
'form_index_error_required',
|
||||
'Пожалуйста, заполните все обязательные поля.'
|
||||
),
|
||||
'old' => $form_index_old,
|
||||
'errors' => $form_index_errors,
|
||||
];
|
||||
|
||||
header('Location: ' . strtok($_SERVER['REQUEST_URI'], '?') . '#concierge-form');
|
||||
exit;
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Labels catégories traduites
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
$categoryLabels = [
|
||||
'lawyer' => t($translations, 'form_index_category_lawyer', 'Адвокат'),
|
||||
'notary' => t($translations, 'form_index_category_notary', 'Нотариус'),
|
||||
'real_estate' => t($translations, 'form_index_category_real_estate', 'Риелтор'),
|
||||
'psychologist' => t($translations, 'form_index_category_psychologist', 'Психолог'),
|
||||
'dentist_ortho' => t($translations, 'form_index_category_dentist_ortho', 'Стоматолог / Ортодонт'),
|
||||
'financial_advisor' => t($translations, 'form_index_category_financial_advisor', 'Финансовый консультант'),
|
||||
'translator' => t($translations, 'form_index_category_translator', 'Переводчик / Присяжный переводчик'),
|
||||
'residence_consultant' => t($translations, 'form_index_category_residence_consultant', 'Консультант по ВНЖ'),
|
||||
'repair' => t($translations, 'form_index_category_repair', 'Мастер по ремонту'),
|
||||
'funeral' => t($translations, 'form_index_category_funeral', 'Похоронное бюро'),
|
||||
'other' => t($translations, 'form_index_category_other', 'Другое'),
|
||||
];
|
||||
|
||||
$contactValue = $form_index_old['contact_type'] === 'email'
|
||||
? $form_index_old['email']
|
||||
: $form_index_old['wa'];
|
||||
|
||||
$contactTypeLabel = $form_index_old['contact_type'] === 'email' ? 'Email' : 'WhatsApp';
|
||||
|
||||
$langCurrent = (string) ($lang ?? 'ru');
|
||||
|
||||
$subject = '[' . strtoupper($langCurrent) . '] Nouvelle demande index — ' . ($categoryLabels[$form_index_old['category']] ?? $form_index_old['category']);
|
||||
|
||||
$html = '
|
||||
<h2>Nouvelle demande depuis le formulaire index</h2>
|
||||
<p><strong>Langue :</strong> ' . htmlspecialchars($langCurrent, ENT_QUOTES, 'UTF-8') . '</p>
|
||||
<p><strong>Nom :</strong> ' . nl2br(htmlspecialchars($form_index_old['name'], ENT_QUOTES, 'UTF-8')) . '</p>
|
||||
<p><strong>Catégorie :</strong> ' . nl2br(htmlspecialchars($categoryLabels[$form_index_old['category']] ?? $form_index_old['category'], ENT_QUOTES, 'UTF-8')) . '</p>
|
||||
<p><strong>Situation :</strong><br>' . nl2br(htmlspecialchars($form_index_old['situation'], ENT_QUOTES, 'UTF-8')) . '</p>
|
||||
<p><strong>Type de contact :</strong> ' . htmlspecialchars($contactTypeLabel, ENT_QUOTES, 'UTF-8') . '</p>
|
||||
<p><strong>Contact :</strong> ' . nl2br(htmlspecialchars($contactValue, ENT_QUOTES, 'UTF-8')) . '</p>
|
||||
';
|
||||
|
||||
$text =
|
||||
"Nouvelle demande depuis le formulaire index\n\n" .
|
||||
"Langue : " . $langCurrent . "\n" .
|
||||
"Nom : " . $form_index_old['name'] . "\n" .
|
||||
"Catégorie : " . ($categoryLabels[$form_index_old['category']] ?? $form_index_old['category']) . "\n" .
|
||||
"Situation : " . $form_index_old['situation'] . "\n" .
|
||||
"Type de contact : " . $contactTypeLabel . "\n" .
|
||||
"Contact : " . $contactValue . "\n";
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Reply-To seulement si email
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
$mailOptions = [];
|
||||
if ($form_index_old['contact_type'] === 'email' && $form_index_old['email'] !== '') {
|
||||
$mailOptions['reply_to'] = [$form_index_old['email'], $form_index_old['name']];
|
||||
}
|
||||
|
||||
[$ok, $err] = uslugi_mail(
|
||||
'contact@uslugi.fr',
|
||||
$subject,
|
||||
$html,
|
||||
$text,
|
||||
$mailOptions
|
||||
);
|
||||
|
||||
if ($ok) {
|
||||
$_SESSION['form_index_flash'] = [
|
||||
'status' => 'success',
|
||||
'message' => t(
|
||||
$translations,
|
||||
'form_index_success_text',
|
||||
'Мы изучим вашу ситуацию и свяжемся с вами в ближайшее время, чтобы предложить подходящего специалиста.'
|
||||
),
|
||||
'old' => [
|
||||
'name' => '',
|
||||
'category' => '',
|
||||
'situation' => '',
|
||||
'contact_type' => 'email',
|
||||
'email' => '',
|
||||
'wa' => '',
|
||||
],
|
||||
'errors' => [],
|
||||
];
|
||||
|
||||
header('Location: ' . strtok($_SERVER['REQUEST_URI'], '?') . '#concierge-form');
|
||||
exit;
|
||||
}
|
||||
|
||||
error_log('Form index send error: ' . (string) $err);
|
||||
|
||||
$_SESSION['form_index_flash'] = [
|
||||
'status' => 'error',
|
||||
'message' => t(
|
||||
$translations,
|
||||
'form_index_error_send',
|
||||
'Произошла ошибка при отправке. Пожалуйста, попробуйте ещё раз.'
|
||||
),
|
||||
'old' => $form_index_old,
|
||||
'errors' => [],
|
||||
];
|
||||
|
||||
header('Location: ' . strtok($_SERVER['REQUEST_URI'], '?') . '#concierge-form');
|
||||
exit;
|
||||
@@ -162,8 +162,6 @@ try {
|
||||
$mot_de_passe_hash = password_hash($mdp, PASSWORD_DEFAULT);
|
||||
$token = bin2hex(random_bytes(32));
|
||||
$token_validation_expire = (new DateTime('+6 hours'))->format('Y-m-d H:i:s');
|
||||
$token_autologin = bin2hex(random_bytes(32));
|
||||
$token_autologin_expire = (new DateTime('+60 minutes'))->format('Y-m-d H:i:s');
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO compte (
|
||||
@@ -173,9 +171,8 @@ try {
|
||||
reference_id,
|
||||
token_validation,
|
||||
token_validation_expire,
|
||||
email_valide,
|
||||
token_autologin,
|
||||
token_autologin_expire
|
||||
email_valide
|
||||
|
||||
)
|
||||
VALUES (
|
||||
:email,
|
||||
@@ -184,9 +181,8 @@ try {
|
||||
:ref_id,
|
||||
:token,
|
||||
:token_expire,
|
||||
0,
|
||||
:al,
|
||||
:al_exp
|
||||
0
|
||||
|
||||
)
|
||||
");
|
||||
$stmt->execute([
|
||||
@@ -195,8 +191,7 @@ try {
|
||||
'ref_id' => $client_id,
|
||||
'token' => $token,
|
||||
'token_expire' => $token_validation_expire,
|
||||
'al' => $token_autologin,
|
||||
'al_exp' => $token_autologin_expire,
|
||||
|
||||
]);
|
||||
|
||||
// Commit avant l’envoi du mail
|
||||
@@ -220,7 +215,7 @@ try {
|
||||
// =====================
|
||||
// Email de validation
|
||||
// =====================
|
||||
$validation_url = "https://uslugi.fr/validation.php?token=" . urlencode($token) . "&al=" . urlencode($token_autologin);
|
||||
$validation_url = "https://uslugi.fr/validation.php?token=" . urlencode($token);
|
||||
$validation_url_html = htmlspecialchars($validation_url, ENT_QUOTES, 'UTF-8');
|
||||
$prenom_nom_safe = htmlspecialchars("$prenom $nom", ENT_QUOTES, 'UTF-8');
|
||||
|
||||
|
||||
@@ -1,292 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/../includes/bootstrap_global.php';
|
||||
|
||||
/**
|
||||
* UI helper: affiche une jolie carte centrée puis exit.
|
||||
* $type: 'error' | 'info' | 'success'
|
||||
*/
|
||||
function ui_exit(string $title, string $message, int $status = 400, string $type = 'error', ?string $cta_href = '/', ?string $cta_label = null): void {
|
||||
http_response_code($status);
|
||||
$title = htmlspecialchars($title, ENT_QUOTES, 'UTF-8');
|
||||
$message = nl2br(htmlspecialchars($message, ENT_QUOTES, 'UTF-8'));
|
||||
$cta_href = $cta_href ? htmlspecialchars($cta_href, ENT_QUOTES, 'UTF-8') : null;
|
||||
$cta_label = $cta_label ?? 'OK';
|
||||
|
||||
// Styles d'icône selon le type
|
||||
$iconBorder = '#B91C1C'; // rouge par défaut (error)
|
||||
$iconBg = 'rgba(185,28,28,.08)';
|
||||
$iconRing = 'rgba(185,28,28,.25)';
|
||||
$checkCSS = ''; // pour success
|
||||
if ($type === 'info') {
|
||||
$iconBorder = '#1D4ED8';
|
||||
$iconBg = 'rgba(29,78,216,.08)';
|
||||
$iconRing = 'rgba(29,78,216,.25)';
|
||||
} elseif ($type === 'success') {
|
||||
$iconBorder = '#1E8050';
|
||||
$iconBg = 'rgba(30,128,80,.08)';
|
||||
$iconRing = 'rgba(30,128,80,.20)';
|
||||
$checkCSS = 'width:48px;height:36px;border:5px solid #1e8050;border-top:0;border-left:0;transform:rotate(45deg);';
|
||||
}
|
||||
|
||||
echo '<!DOCTYPE html><html lang="fr"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">';
|
||||
echo '<title>'.$title.'</title>';
|
||||
echo '<style>
|
||||
html,body{height:100%;margin:0}
|
||||
body{display:flex;align-items:center;justify-content:center;background:#8CA9FF;font-family:Montserrat,system-ui,Segoe UI,Roboto,Arial}
|
||||
.card{max-width:720px;width:94%;background:#fff;border:1px solid #e5e7eb;border-radius:14px;box-shadow:0 8px 24px rgba(0,0,0,.08);padding:28px;text-align:center}
|
||||
h1{margin:6px 0 10px;font-size:clamp(22px,2.4vw,28px)}
|
||||
p{margin:0 0 18px;color:#6b7280;font-size:clamp(16px,2.2vw,18px)}
|
||||
.row{display:flex;gap:10px;justify-content:center;flex-wrap:wrap;margin-top:8px}
|
||||
.btn{padding:10px 14px;border-radius:10px;text-decoration:none;display:inline-block}
|
||||
.btn-primary{background:#2563eb;color:#fff}
|
||||
.btn-outline{border:1px solid #d1d5db;color:#374151;background:#fff}
|
||||
</style></head><body><div class="card">';
|
||||
|
||||
// Icône
|
||||
echo '<div style="width:102px;height:102px;border-radius:50%;background:'.$iconBg.';border:3px solid '.$iconRing.';display:flex;align-items:center;justify-content:center;margin:0 auto 20px;">';
|
||||
if ($type === 'success') {
|
||||
echo '<div style="'.$checkCSS.'"></div>';
|
||||
} elseif ($type === 'info') {
|
||||
echo '<div style="width:14px;height:48px;border-left:5px solid '.$iconBorder.';position:relative;">
|
||||
<div style="position:absolute;top:-18px;left:-5px;width:10px;height:10px;background:'.$iconBorder.';border-radius:50%;"></div>
|
||||
</div>';
|
||||
} else { // error
|
||||
echo '<div style="position:relative;width:48px;height:48px;">
|
||||
<div style="position:absolute;top:0;left:22px;width:5px;height:48px;background:'.$iconBorder.';transform:rotate(45deg);border-radius:2px;"></div>
|
||||
<div style="position:absolute;top:0;left:22px;width:5px;height:48px;background:'.$iconBorder.';transform:rotate(-45deg);border-radius:2px;"></div>
|
||||
</div>';
|
||||
}
|
||||
echo '</div>';
|
||||
|
||||
echo '<h1>'.$title.'</h1>';
|
||||
echo '<p>'.$message.'</p>';
|
||||
|
||||
if ($cta_href) {
|
||||
echo '<div class="row"><a class="btn btn-outline" href="'.$cta_href.'">'.$cta_label.'</a></div>';
|
||||
}
|
||||
|
||||
echo '</div></body></html>';
|
||||
exit;
|
||||
}
|
||||
|
||||
// =====================
|
||||
// Accès direct interdit
|
||||
// =====================
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
|
||||
ui_exit(
|
||||
$translations['err_acces_incorrect_titre'] ?? 'Accès incorrect',
|
||||
$translations['err_acces_incorrect_msg'] ?? "Cette page doit être appelée depuis le formulaire d'inscription.",
|
||||
405,
|
||||
'info',
|
||||
'inscription_client.php',
|
||||
$translations['retour_formulaire'] ?? 'Retour au formulaire'
|
||||
);
|
||||
}
|
||||
|
||||
// =====================
|
||||
// Données & vérifs
|
||||
// =====================
|
||||
$nom = trim($_POST['nom'] ?? '');
|
||||
$prenom = trim($_POST['prenom'] ?? '');
|
||||
$email = trim($_POST['email'] ?? '');
|
||||
$mdp = $_POST['mot_de_passe'] ?? '';
|
||||
|
||||
// Champs obligatoires
|
||||
if ($nom === '' || $prenom === '' || $email === '' || $mdp === '') {
|
||||
ui_exit(
|
||||
$translations['err_titre'] ?? 'Un problème est survenu',
|
||||
$translations['err_champs_obligatoires'] ?? 'Merci de remplir tous les champs obligatoires.',
|
||||
400,
|
||||
'error',
|
||||
'inscription_client.php',
|
||||
$translations['retour_formulaire'] ?? 'Retour au formulaire'
|
||||
);
|
||||
}
|
||||
|
||||
// Email valide + pas d’injection CR/LF
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL) || preg_match('/[\r\n]/', $email)) {
|
||||
ui_exit(
|
||||
$translations['err_titre'] ?? 'Un problème est survenu',
|
||||
$translations['err_email_non_valide'] ?? 'Email invalide.',
|
||||
400,
|
||||
'error',
|
||||
'inscription_client.php'
|
||||
);
|
||||
}
|
||||
|
||||
// Acceptation CGU
|
||||
$accept_cgu = isset($_POST['accept_cgu']) ? 1 : 0;
|
||||
if ($accept_cgu !== 1) {
|
||||
ui_exit(
|
||||
$translations['err_titre'] ?? 'Un problème est survenu',
|
||||
$translations['cgu_obligatoire'] ?? 'Vous devez accepter les CGU.',
|
||||
400,
|
||||
'info',
|
||||
'inscription_client.php'
|
||||
);
|
||||
}
|
||||
|
||||
// =====================
|
||||
// Transaction (client + compte)
|
||||
// =====================
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
|
||||
// Email déjà pris ?
|
||||
$stmt = $pdo->prepare("SELECT id FROM compte WHERE email = :email LIMIT 1");
|
||||
$stmt->execute(['email' => $email]);
|
||||
if ($stmt->fetch()) {
|
||||
$pdo->rollBack();
|
||||
ui_exit(
|
||||
$translations['err_titre'] ?? 'Un problème est survenu',
|
||||
$translations['err_email_deja_utilise'] ?? 'Cet email est déjà utilisé.',
|
||||
409,
|
||||
'error',
|
||||
'inscription_client.php'
|
||||
);
|
||||
}
|
||||
|
||||
// 1) Insert client
|
||||
$stmt = $pdo->prepare("INSERT INTO client (nom, prenom, email, accepted_cgu) VALUES (:nom, :prenom, :email, :accepted_cgu)");
|
||||
$stmt->execute([
|
||||
'nom' => $nom,
|
||||
'prenom' => $prenom,
|
||||
'email' => $email,
|
||||
'accepted_cgu' => $accept_cgu
|
||||
]);
|
||||
$client_id = (int)$pdo->lastInsertId();
|
||||
|
||||
// 2) Créer compte
|
||||
$mot_de_passe_hash = password_hash($mdp, PASSWORD_DEFAULT);
|
||||
$token = bin2hex(random_bytes(32));
|
||||
$token_autologin = bin2hex(random_bytes(32));
|
||||
$token_autologin_expire = (new DateTime('+60 minutes'))->format('Y-m-d H:i:s');
|
||||
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO compte (email, mot_de_passe, role, reference_id, token_validation, email_valide, token_autologin, token_autologin_expire)
|
||||
VALUES (:email, :mdp, 'client', :ref_id, :token, 0, :al, :al_exp)
|
||||
");
|
||||
$stmt->execute([
|
||||
'email' => $email,
|
||||
'mdp' => $mot_de_passe_hash,
|
||||
'ref_id' => $client_id,
|
||||
'token' => $token,
|
||||
'al' => $token_autologin,
|
||||
'al_exp' => $token_autologin_expire,
|
||||
]);
|
||||
|
||||
|
||||
|
||||
// Commit avant l’envoi du mail
|
||||
$pdo->commit();
|
||||
|
||||
} catch (Throwable $e) {
|
||||
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||
error_log('Inscription client - DB error: '.$e->getMessage());
|
||||
ui_exit(
|
||||
$translations['err_titre'] ?? 'Un problème est survenu',
|
||||
$translations['err_serveur'] ?? 'Erreur serveur.',
|
||||
500,
|
||||
'error',
|
||||
'/'
|
||||
);
|
||||
}
|
||||
|
||||
// =====================
|
||||
// Email de validation
|
||||
// =====================
|
||||
$validation_url = "https://uslugi.fr/validation.php?token=" . urlencode($token) . "&al=" . urlencode($token_autologin);
|
||||
$validation_url_html = htmlspecialchars($validation_url, ENT_QUOTES, 'UTF-8');
|
||||
$prenom_nom_safe = htmlspecialchars("$prenom $nom", ENT_QUOTES, 'UTF-8');
|
||||
|
||||
$sujet = $translations['mail_valid_sujet'];
|
||||
|
||||
$html = "<!DOCTYPE html>
|
||||
<html lang='fr'>
|
||||
<head>
|
||||
<meta charset='UTF-8'>
|
||||
<meta http-equiv='X-UA-Compatible' content='IE=edge'>
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; background-color:#f4f4f4; padding:20px; }
|
||||
.mail-container { max-width:600px; margin:auto; background:#fff; padding:30px; border-radius:8px; box-shadow:0 0 10px rgba(0,0,0,0.1); }
|
||||
.title { font-size:20px; font-weight:bold; color:#333; margin-bottom:20px; }
|
||||
.btn { display:inline-block; margin-top:20px; padding:12px 24px; background-color:#E03C3F; color:#fff; text-decoration:none; border-radius:999px; font-weight:bold; }
|
||||
.footer { margin-top:30px; font-size:13px; color:#888; }
|
||||
.link-fallback { word-break: break-all; font-size:13px; margin-top:15px; color:#555; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class='mail-container'>
|
||||
<div class='title'>".sprintf($translations['mail_valid_bonjour_nom'], $prenom_nom_safe)."</div>
|
||||
<p>".$translations['mail_valid_merci']."</p>
|
||||
<p>".$translations['mail_valid_action']."</p>
|
||||
<a href='$validation_url_html' class='btn'>".$translations['mail_valid_btn']."</a>
|
||||
<p class='link-fallback'>
|
||||
".$translations['mail_valid_fallback']."<br>
|
||||
<a href='$validation_url_html'>$validation_url_html</a>
|
||||
</p>
|
||||
<div class='footer'>
|
||||
".$translations['mail_valid_expire']."<br>
|
||||
".$translations['mail_valid_ignore']."<br><br>
|
||||
".$translations['mail_valid_signature']."
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>";
|
||||
|
||||
$text = strip_tags(
|
||||
sprintf($translations['mail_valid_bonjour_nom'], "$prenom $nom")."\n\n".
|
||||
$translations['mail_valid_merci']."\n".
|
||||
$translations['mail_valid_action']."\n".
|
||||
($translations['mail_valid_fallback'] ?? 'Si le bouton ne fonctionne pas, utilisez ce lien :')."\n".
|
||||
$validation_url."\n\n".
|
||||
$translations['mail_valid_expire']."\n".
|
||||
$translations['mail_valid_ignore']."\n".
|
||||
($translations['mail_valid_signature'] ?? 'USLUGI.FR')
|
||||
);
|
||||
|
||||
// Envoi du mail (non bloquant)
|
||||
list($sent, $err) = uslugi_mail(
|
||||
$email,
|
||||
$sujet,
|
||||
$html,
|
||||
$text,
|
||||
['to_name' => "$prenom $nom"]
|
||||
);
|
||||
if (!$sent) {
|
||||
error_log("Validation mail NOT sent to $email : $err");
|
||||
// On ne bloque pas l'utilisateur.
|
||||
}
|
||||
|
||||
// =====================
|
||||
// Écran final (succès)
|
||||
// =====================
|
||||
echo '
|
||||
<!DOCTYPE html><html lang="fr"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>'.htmlspecialchars($translations['insc_merci_titre'] ?? 'Merci !', ENT_QUOTES, 'UTF-8').'</title>
|
||||
<style>
|
||||
html,body{height:100%;margin:0}
|
||||
body{display:flex;align-items:center;justify-content:center;background:#8CA9FF;font-family:Montserrat,system-ui,Segoe UI,Roboto,Arial}
|
||||
.card{max-width:600px;width:98%;background:#fff;border:1px solid #e5e7eb;border-radius:14px;box-shadow:0 8px 24px rgba(0,0,0,.08);padding:28px;text-align:center}
|
||||
h1{margin:6px 0 10px;font-size:clamp(22px,2.4vw,28px)}
|
||||
p{margin:0 0 18px;color:#6b7280;font-size:clamp(16px,2.2vw,18px)}
|
||||
.row{display:flex;gap:10px;justify-content:center;flex-wrap:wrap}
|
||||
.btn{padding:10px 14px;border-radius:10px;text-decoration:none;display:inline-block}
|
||||
.btn-primary{background:#2563eb;color:#fff}
|
||||
.btn-outline{border:1px solid #d1d5db;color:#374151;background:#fff}
|
||||
</style></head><body>
|
||||
<div class="card">
|
||||
<div style="width:102px;height:102px;border-radius:50%;background:rgba(30,128,80,.08);border:3px solid rgba(30,128,80,.20);display:flex;align-items:center;justify-content:center;margin:0 auto 20px;">
|
||||
<div style="width:48px;height:36px;border:5px solid #1e8050;border-top:0;border-left:0;transform:rotate(45deg);"></div>
|
||||
</div>
|
||||
<h1>'.htmlspecialchars($translations['insc_merci_titre'] ?? 'Merci !', ENT_QUOTES, 'UTF-8').'</h1>
|
||||
<p>'.htmlspecialchars($translations['insc_merci_ligne1'] ?? 'Votre compte a bien été créé.', ENT_QUOTES, 'UTF-8').'</p>
|
||||
<p>'.htmlspecialchars($translations['insc_merci_ligne2'] ?? 'Vérifiez vos emails pour valider votre adresse.', ENT_QUOTES, 'UTF-8').'</p>
|
||||
<div class="row">
|
||||
<a class="btn btn-outline" href="/">'.htmlspecialchars($translations['retour_acceuil'] ?? 'Retour à l’accueil', ENT_QUOTES, 'UTF-8').'</a>
|
||||
</div>
|
||||
</div>
|
||||
</body></html>';
|
||||
@@ -241,8 +241,6 @@ try {
|
||||
$mot_de_passe_hash = password_hash($mdp, PASSWORD_DEFAULT);
|
||||
$token = bin2hex(random_bytes(32));
|
||||
$token_validation_expire = (new DateTime('+6 hours'))->format('Y-m-d H:i:s');
|
||||
$token_autologin = bin2hex(random_bytes(32));
|
||||
$token_autologin_expire = (new DateTime('+60 minutes'))->format('Y-m-d H:i:s');
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO compte (
|
||||
@@ -252,9 +250,8 @@ try {
|
||||
reference_id,
|
||||
token_validation,
|
||||
token_validation_expire,
|
||||
email_valide,
|
||||
token_autologin,
|
||||
token_autologin_expire
|
||||
email_valide
|
||||
|
||||
)
|
||||
VALUES (
|
||||
:email,
|
||||
@@ -263,9 +260,8 @@ try {
|
||||
:ref_id,
|
||||
:token,
|
||||
:token_expire,
|
||||
0,
|
||||
:al,
|
||||
:al_exp
|
||||
0
|
||||
|
||||
)
|
||||
");
|
||||
$stmt->execute([
|
||||
@@ -274,8 +270,7 @@ try {
|
||||
'ref_id' => $praticien_id,
|
||||
'token' => $token,
|
||||
'token_expire' => $token_validation_expire,
|
||||
'al' => $token_autologin,
|
||||
'al_exp' => $token_autologin_expire,
|
||||
|
||||
]);
|
||||
|
||||
// Commit avant envoi mail
|
||||
@@ -299,7 +294,7 @@ try {
|
||||
// =====================
|
||||
// Email de validation
|
||||
// =====================
|
||||
$validation_url = "https://uslugi.fr/validation.php?token=" . urlencode($token) . "&al=" . urlencode($token_autologin);
|
||||
$validation_url = "https://uslugi.fr/validation.php?token=" . urlencode($token);
|
||||
$validation_url_html = htmlspecialchars($validation_url, ENT_QUOTES, 'UTF-8');
|
||||
$prenom_nom = htmlspecialchars("$prenom $nom", ENT_QUOTES, 'UTF-8');
|
||||
|
||||
|
||||
@@ -1,370 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/../includes/bootstrap_global.php';
|
||||
|
||||
/**
|
||||
* UI helper: affiche une jolie carte centrée puis exit.
|
||||
* $type: 'error' | 'info' | 'success'
|
||||
*/
|
||||
function ui_exit(string $title, string $message, int $status = 400, string $type = 'error', ?string $cta_href = '/', ?string $cta_label = null): void {
|
||||
http_response_code($status);
|
||||
$title = htmlspecialchars($title, ENT_QUOTES, 'UTF-8');
|
||||
$message = nl2br(htmlspecialchars($message, ENT_QUOTES, 'UTF-8'));
|
||||
$cta_href = $cta_href ? htmlspecialchars($cta_href, ENT_QUOTES, 'UTF-8') : null;
|
||||
$cta_label = $cta_label ?? 'OK';
|
||||
|
||||
// Styles d'icône selon le type
|
||||
$iconBorder = '#B91C1C'; // rouge par défaut (error)
|
||||
$iconBg = 'rgba(185,28,28,.08)';
|
||||
$iconRing = 'rgba(185,28,28,.25)';
|
||||
$checkCSS = ''; // pour success
|
||||
if ($type === 'info') {
|
||||
$iconBorder = '#1D4ED8';
|
||||
$iconBg = 'rgba(29,78,216,.08)';
|
||||
$iconRing = 'rgba(29,78,216,.25)';
|
||||
} elseif ($type === 'success') {
|
||||
$iconBorder = '#1E8050';
|
||||
$iconBg = 'rgba(30,128,80,.08)';
|
||||
$iconRing = 'rgba(30,128,80,.20)';
|
||||
$checkCSS = 'width:48px;height:36px;border:5px solid #1e8050;border-top:0;border-left:0;transform:rotate(45deg);';
|
||||
}
|
||||
|
||||
echo '<!DOCTYPE html><html lang="fr"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">';
|
||||
echo '<title>'.$title.'</title>';
|
||||
echo '<style>
|
||||
html,body{height:100%;margin:0}
|
||||
body{display:flex;align-items:center;justify-content:center;background:#8CA9FF;font-family:Montserrat,system-ui,Segoe UI,Roboto,Arial}
|
||||
.card{max-width:720px;width:94%;background:#fff;border:1px solid #e5e7eb;border-radius:14px;box-shadow:0 8px 24px rgba(0,0,0,.08);padding:28px;text-align:center}
|
||||
h1{margin:6px 0 10px;font-size:clamp(22px,2.4vw,28px)}
|
||||
p{margin:0 0 18px;color:#6b7280;font-size:clamp(16px,2.2vw,18px)}
|
||||
.row{display:flex;gap:10px;justify-content:center;flex-wrap:wrap;margin-top:8px}
|
||||
.btn{padding:10px 14px;border-radius:10px;text-decoration:none;display:inline-block}
|
||||
.btn-primary{background:#2563eb;color:#fff}
|
||||
.btn-outline{border:1px solid #d1d5db;color:#374151;background:#fff}
|
||||
</style></head><body><div class="card">';
|
||||
|
||||
// Icône
|
||||
echo '<div style="width:102px;height:102px;border-radius:50%;background:'.$iconBg.';border:3px solid '.$iconRing.';display:flex;align-items:center;justify-content:center;margin:0 auto 20px;">';
|
||||
if ($type === 'success') {
|
||||
echo '<div style="'.$checkCSS.'"></div>';
|
||||
} elseif ($type === 'info') {
|
||||
echo '<div style="width:14px;height:48px;border-left:5px solid '.$iconBorder.';position:relative;">
|
||||
<div style="position:absolute;top:-18px;left:-5px;width:10px;height:10px;background:'.$iconBorder.';border-radius:50%;"></div>
|
||||
</div>';
|
||||
} else { // error
|
||||
echo '<div style="position:relative;width:48px;height:48px;">
|
||||
<div style="position:absolute;top:0;left:22px;width:5px;height:48px;background:'.$iconBorder.';transform:rotate(45deg);border-radius:2px;"></div>
|
||||
<div style="position:absolute;top:0;left:22px;width:5px;height:48px;background:'.$iconBorder.';transform:rotate(-45deg);border-radius:2px;"></div>
|
||||
</div>';
|
||||
}
|
||||
echo '</div>';
|
||||
|
||||
echo '<h1>'.$title.'</h1>';
|
||||
echo '<p>'.$message.'</p>';
|
||||
|
||||
if ($cta_href) {
|
||||
echo '<div class="row"><a class="btn btn-outline" href="'.$cta_href.'">'.$cta_label.'</a></div>';
|
||||
}
|
||||
|
||||
echo '</div></body></html>';
|
||||
exit;
|
||||
}
|
||||
|
||||
// =====================
|
||||
// Sécurité de base
|
||||
// =====================
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
|
||||
ui_exit(
|
||||
$translations['err_acces_incorrect_titre'] ?? 'Accès incorrect',
|
||||
$translations['err_acces_incorrect_msg'] ?? "Cette page doit être appelée depuis le formulaire d'inscription.",
|
||||
405,
|
||||
'info',
|
||||
'inscription_praticien.php',
|
||||
$translations['retour_formulaire'] ?? 'Retour au formulaire'
|
||||
);
|
||||
}
|
||||
|
||||
// =====================
|
||||
// Récupération & checks
|
||||
// =====================
|
||||
$nom = trim($_POST['nom'] ?? '');
|
||||
$prenom = trim($_POST['prenom'] ?? '');
|
||||
$email = trim($_POST['email'] ?? '');
|
||||
$mdp = $_POST['mot_de_passe'] ?? '';
|
||||
$siret = preg_replace('/\D/', '', $_POST['siret'] ?? '');
|
||||
|
||||
// Champs obligatoires
|
||||
if ($nom === '' || $prenom === '' || $email === '' || $mdp === '') {
|
||||
ui_exit(
|
||||
$translations['err_titre'] ?? 'Un problème est survenu',
|
||||
$translations['err_champs_obligatoires'] ?? 'Merci de remplir tous les champs obligatoires.',
|
||||
400,
|
||||
'error',
|
||||
'inscription_praticien.php',
|
||||
$translations['retour_formulaire'] ?? 'Retour au formulaire'
|
||||
);
|
||||
}
|
||||
|
||||
// Email valide + pas d'injection d'entêtes
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL) || preg_match('/[\r\n]/', $email)) {
|
||||
ui_exit(
|
||||
$translations['err_titre'] ?? 'Un problème est survenu',
|
||||
$translations['err_email_non_valide'] ?? 'Email invalide.',
|
||||
400,
|
||||
'error',
|
||||
'inscription_praticien.php'
|
||||
);
|
||||
}
|
||||
|
||||
// SIRET longueur
|
||||
if (strlen($siret) !== 14) {
|
||||
ui_exit(
|
||||
$translations['err_titre'] ?? 'Un problème est survenu',
|
||||
$translations['err_siret_invalide'] ?? 'Numéro SIRET invalide (14 chiffres requis).',
|
||||
400,
|
||||
'error',
|
||||
'inscription_praticien.php'
|
||||
);
|
||||
}
|
||||
|
||||
// Luhn
|
||||
function luhnCheck($num) {
|
||||
$arr = str_split(strrev($num));
|
||||
$sum = 0;
|
||||
foreach ($arr as $i => $n) {
|
||||
$n = (int)$n;
|
||||
if ($i % 2) {
|
||||
$n *= 2;
|
||||
if ($n > 9) $n -= 9;
|
||||
}
|
||||
$sum += $n;
|
||||
}
|
||||
return $sum % 10 === 0;
|
||||
}
|
||||
if (!luhnCheck($siret)) {
|
||||
ui_exit(
|
||||
$translations['err_titre'] ?? 'Un problème est survenu',
|
||||
$translations['err_siret_invalide'] ?? 'Numéro SIRET invalide (échec vérification).',
|
||||
400,
|
||||
'error',
|
||||
'inscription_praticien.php'
|
||||
);
|
||||
}
|
||||
|
||||
// =====================
|
||||
// Transaction
|
||||
// =====================
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
|
||||
// Email déjà utilisé ?
|
||||
$stmt = $pdo->prepare("SELECT id FROM compte WHERE email = :email LIMIT 1");
|
||||
$stmt->execute(['email' => $email]);
|
||||
if ($stmt->fetch()) {
|
||||
$pdo->rollBack();
|
||||
ui_exit(
|
||||
$translations['err_titre'] ?? 'Un problème est survenu',
|
||||
$translations['err_email_deja_utilise'] ?? 'Cet email est déjà utilisé.',
|
||||
409,
|
||||
'error',
|
||||
'inscription_praticien.php'
|
||||
);
|
||||
}
|
||||
|
||||
// SIRET déjà utilisé ?
|
||||
$stmt = $pdo->prepare("SELECT id FROM praticien WHERE siret = :siret LIMIT 1");
|
||||
$stmt->execute(['siret' => $siret]);
|
||||
if ($stmt->fetch()) {
|
||||
$pdo->rollBack();
|
||||
ui_exit(
|
||||
$translations['err_titre'] ?? 'Un problème est survenu',
|
||||
$translations['err_siret_deja_utilise'] ?? 'Ce numéro SIRET est déjà enregistré.',
|
||||
409,
|
||||
'error',
|
||||
'inscription_praticien.php'
|
||||
);
|
||||
}
|
||||
|
||||
// Acceptation CGU
|
||||
$accept_cgu = isset($_POST['accept_cgu']) ? 1 : 0;
|
||||
if ($accept_cgu !== 1) {
|
||||
$pdo->rollBack();
|
||||
ui_exit(
|
||||
$translations['err_titre'] ?? 'Un problème est survenu',
|
||||
$translations['cgu_obligatoire'] ?? 'Vous devez accepter les CGU.',
|
||||
400,
|
||||
'info',
|
||||
'inscription_praticien.php'
|
||||
);
|
||||
}
|
||||
|
||||
// 1) Préparer la date d'hier pour forcer l'expiration immédiate
|
||||
$hier = date('Y-m-d', strtotime('-1 day'));
|
||||
|
||||
// 2) Créer le praticien avec les champs d'abonnement initialisés
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO praticien (
|
||||
nom,
|
||||
prenom,
|
||||
email,
|
||||
siret,
|
||||
accepted_cgu,
|
||||
abonnement_id,
|
||||
date_expiration
|
||||
) VALUES (
|
||||
:nom,
|
||||
:prenom,
|
||||
:email,
|
||||
:siret,
|
||||
:accepted_cgu,
|
||||
NULL,
|
||||
:date_exp
|
||||
)
|
||||
");
|
||||
|
||||
$stmt->execute([
|
||||
'nom' => $nom,
|
||||
'prenom' => $prenom,
|
||||
'email' => $email,
|
||||
'siret' => $siret,
|
||||
'accepted_cgu' => $accept_cgu,
|
||||
'date_exp' => $hier // On met hier pour que le script de vérification dise "C'est expiré !"
|
||||
]);
|
||||
$praticien_id = (int)$pdo->lastInsertId();
|
||||
|
||||
// 2) Créer le compte
|
||||
$mot_de_passe_hash = password_hash($mdp, PASSWORD_DEFAULT);
|
||||
$token = bin2hex(random_bytes(32));
|
||||
$token_autologin = bin2hex(random_bytes(32));
|
||||
$token_autologin_expire = (new DateTime('+60 minutes'))->format('Y-m-d H:i:s');
|
||||
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO compte (email, mot_de_passe, role, reference_id, token_validation, email_valide, token_autologin, token_autologin_expire)
|
||||
VALUES (:email, :mdp, 'praticien', :ref_id, :token, 0, :al, :al_exp)
|
||||
");
|
||||
$stmt->execute([
|
||||
'email' => $email,
|
||||
'mdp' => $mot_de_passe_hash,
|
||||
'ref_id' => $praticien_id,
|
||||
'token' => $token,
|
||||
'al' => $token_autologin,
|
||||
'al_exp' => $token_autologin_expire,
|
||||
]);
|
||||
|
||||
|
||||
// Commit avant envoi mail
|
||||
$pdo->commit();
|
||||
|
||||
} catch (Throwable $e) {
|
||||
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||
error_log('Inscription praticien - DB error: '.$e->getMessage());
|
||||
ui_exit(
|
||||
$translations['err_titre'] ?? 'Un problème est survenu',
|
||||
$translations['err_serveur'] ?? 'Erreur serveur.',
|
||||
500,
|
||||
'error',
|
||||
'/'
|
||||
);
|
||||
}
|
||||
|
||||
// =====================
|
||||
// Email de validation
|
||||
// =====================
|
||||
$validation_url = "https://uslugi.fr/validation.php?token=" . urlencode($token) . "&al=" . urlencode($token_autologin);
|
||||
$validation_url_html = htmlspecialchars($validation_url, ENT_QUOTES, 'UTF-8');
|
||||
$prenom_nom = htmlspecialchars("$prenom $nom", ENT_QUOTES, 'UTF-8');
|
||||
|
||||
$sujet = $translations['mail_valid_sujet'];
|
||||
|
||||
// Corps HTML mail
|
||||
$html = "<!DOCTYPE html>
|
||||
<html lang='fr'>
|
||||
<head>
|
||||
<meta charset='UTF-8'>
|
||||
<meta http-equiv='X-UA-Compatible' content='IE=edge'>
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; background-color:#f4f4f4; padding:20px; }
|
||||
.mail-container { max-width:600px; margin:auto; background:#fff; padding:30px; border-radius:8px; box-shadow:0 0 10px rgba(0,0,0,0.1); }
|
||||
.title { font-size:20px; font-weight:bold; color:#333; margin-bottom:20px; }
|
||||
.btn { display:inline-block; margin-top:20px; padding:12px 24px; background-color:#E03C3F; color:#fff; text-decoration:none; border-radius:999px; font-weight:bold; }
|
||||
.footer { margin-top:30px; font-size:13px; color:#888; }
|
||||
.link-fallback { word-break: break-all; font-size:13px; margin-top:15px; color:#555; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class='mail-container'>
|
||||
<div class='title'>".sprintf($translations['mail_valid_bonjour_nom'], $prenom_nom)."</div>
|
||||
<p>".$translations['mail_valid_merci_praticien']."</p>
|
||||
<p>".$translations['mail_valid_action']."</p>
|
||||
<a href='$validation_url_html' class='btn'>".$translations['mail_valid_btn']."</a>
|
||||
<p class='link-fallback'>".$translations['mail_valid_fallback']."<br>
|
||||
<a href='$validation_url_html'>$validation_url_html</a>
|
||||
</p>
|
||||
<div class='footer'>
|
||||
".$translations['mail_valid_expire']."<br>
|
||||
".$translations['mail_valid_ignore']."<br><br>
|
||||
".$translations['mail_valid_signature']."
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>";
|
||||
|
||||
// Texte alternative
|
||||
$text = strip_tags(
|
||||
sprintf($translations['mail_valid_bonjour_nom'], "$prenom $nom")."\n\n".
|
||||
$translations['mail_valid_merci_praticien']."\n".
|
||||
$translations['mail_valid_action']."\n".
|
||||
($translations['mail_valid_fallback'] ?? 'Si le bouton ne fonctionne pas, utilisez ce lien :')."\n".
|
||||
$validation_url."\n\n".
|
||||
$translations['mail_valid_expire']."\n".
|
||||
$translations['mail_valid_ignore']."\n".
|
||||
($translations['mail_valid_signature'] ?? 'USLUGI.FR')
|
||||
);
|
||||
|
||||
// Envoi
|
||||
list($sent, $err) = uslugi_mail(
|
||||
$email,
|
||||
$sujet,
|
||||
$html,
|
||||
$text,
|
||||
[
|
||||
'to_name' => "$prenom $nom",
|
||||
]
|
||||
);
|
||||
|
||||
if (!$sent) {
|
||||
error_log("Validation mail NOT sent to $email : $err");
|
||||
// On ne bloque pas l’utilisateur : il pourra redemander un mail.
|
||||
}
|
||||
|
||||
// =====================
|
||||
// Écran final succès
|
||||
// =====================
|
||||
echo '
|
||||
<!DOCTYPE html><html lang="fr"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>'.htmlspecialchars($translations['insc_merci_titre'] ?? 'Merci !', ENT_QUOTES, 'UTF-8').'</title>
|
||||
<style>
|
||||
html,body{height:100%;margin:0}
|
||||
body{display:flex;align-items:center;justify-content:center;background:#8CA9FF;font-family:Montserrat,system-ui,Segoe UI,Roboto,Arial}
|
||||
.card{max-width:600px;width:98%;background:#fff;border:1px solid #e5e7eb;border-radius:14px;box-shadow:0 8px 24px rgba(0,0,0,.08);padding:28px;text-align:center}
|
||||
h1{margin:6px 0 10px;font-size:clamp(22px,2.4vw,28px)}
|
||||
p{margin:0 0 18px;color:#6b7280;font-size:clamp(16px,2.2vw,18px)}
|
||||
.row{display:flex;gap:10px;justify-content:center;flex-wrap:wrap}
|
||||
.btn{padding:10px 14px;border-radius:10px;text-decoration:none;display:inline-block}
|
||||
.btn-primary{background:#2563eb;color:#fff}
|
||||
.btn-outline{border:1px solid #d1d5db;color:#374151;background:#fff}
|
||||
</style></head><body>
|
||||
<div class="card">
|
||||
<div style="width:102px;height:102px;border-radius:50%;background:rgba(30,128,80,.08);border:3px solid rgba(30,128,80,.20);display:flex;align-items:center;justify-content:center;margin:0 auto 20px;">
|
||||
<div style="width:48px;height:36px;border:5px solid #1e8050;border-top:0;border-left:0;transform:rotate(45deg);"></div>
|
||||
</div>
|
||||
<h1>'.htmlspecialchars($translations['insc_merci_titre'] ?? 'Merci !', ENT_QUOTES, 'UTF-8').'</h1>
|
||||
<p>'.htmlspecialchars($translations['insc_merci_ligne1'] ?? 'Votre compte a bien été créé.', ENT_QUOTES, 'UTF-8').'</p>
|
||||
<p>'.htmlspecialchars($translations['insc_merci_ligne2'] ?? 'Vérifiez vos emails pour valider votre adresse.', ENT_QUOTES, 'UTF-8').'</p>
|
||||
<div class="row">
|
||||
<a class="btn btn-outline" href="/">'.htmlspecialchars($translations['retour_acceuil'] ?? 'Retour à l’accueil', ENT_QUOTES, 'UTF-8').'</a>
|
||||
</div>
|
||||
</div>
|
||||
</body></html>';
|
||||
+25
-62
@@ -75,10 +75,6 @@ function ui_exit(string $title, string $message, int $status = 400, string $type
|
||||
// 1) Token présent & valide
|
||||
// =====================
|
||||
$rawToken = trim((string) ($_GET['token'] ?? ''));
|
||||
$rawAL = trim((string) ($_GET['al'] ?? ''));
|
||||
if ($rawAL !== '' && ! preg_match('/^[A-Fa-f0-9]{64}$/', $rawAL)) {
|
||||
$rawAL = ''; // on ignore si format suspect
|
||||
}
|
||||
|
||||
if ($rawToken === '') {
|
||||
ui_exit(
|
||||
@@ -157,12 +153,14 @@ if ($now > $expireAt) {
|
||||
// =====================
|
||||
try {
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE compte
|
||||
SET email_valide = 1,
|
||||
token_validation = NULL,
|
||||
token_validation_expire = NULL
|
||||
WHERE id = :id
|
||||
");
|
||||
UPDATE compte
|
||||
SET email_valide = 1,
|
||||
token_validation = NULL,
|
||||
token_validation_expire = NULL,
|
||||
token_autologin = NULL,
|
||||
token_autologin_expire = NULL
|
||||
WHERE id = :id
|
||||
");
|
||||
$stmt->execute(['id' => $compte['id']]);
|
||||
} catch (Throwable $e) {
|
||||
error_log('Validation email - UPDATE échouée: ' . $e->getMessage());
|
||||
@@ -175,63 +173,28 @@ try {
|
||||
$translations['retour_acceuil'] ?? 'Retour à l’accueil'
|
||||
);
|
||||
}
|
||||
|
||||
// =====================
|
||||
// 4bis) Auto-login (si al fourni et valide)
|
||||
// 4bis) Auto-login direct après validation
|
||||
// =====================
|
||||
if ($rawAL !== '') {
|
||||
// On recharge le compte pour lire token_autologin + expire (plus propre)
|
||||
$stmt = $pdo->prepare("SELECT id, role, reference_id, token_autologin, token_autologin_expire
|
||||
FROM compte
|
||||
WHERE id = :id
|
||||
LIMIT 1");
|
||||
$stmt->execute(['id' => (int) $compte['id']]);
|
||||
$c = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
session_regenerate_id(true);
|
||||
$_SESSION['compte_id'] = (int) $compte['id'];
|
||||
$_SESSION['role'] = (string) $compte['role'];
|
||||
$_SESSION['reference_id'] = (int) $compte['reference_id'];
|
||||
|
||||
if ($c && ! empty($c['token_autologin']) && hash_equals((string) $c['token_autologin'], $rawAL)) {
|
||||
|
||||
// Expiration (obligatoire)
|
||||
$expOk = false;
|
||||
if (! empty($c['token_autologin_expire'])) {
|
||||
try {
|
||||
$exp = new DateTime((string) $c['token_autologin_expire']);
|
||||
$expOk = (new DateTime()) <= $exp;
|
||||
} catch (Throwable $e) {
|
||||
$expOk = false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($expOk) {
|
||||
// One-time : on invalide le token autologin
|
||||
$pdo->prepare("UPDATE compte
|
||||
SET token_autologin = NULL, token_autologin_expire = NULL
|
||||
WHERE id = :id")
|
||||
->execute(['id' => (int) $c['id']]);
|
||||
|
||||
// Connexion session (même logique pour client/praticien)
|
||||
session_regenerate_id(true);
|
||||
$_SESSION['compte_id'] = (int) $c['id']; // <-- IMPORTANT (secure.php l'exige)
|
||||
$_SESSION['role'] = (string) $c['role'];
|
||||
$_SESSION['reference_id'] = (int) $c['reference_id'];
|
||||
|
||||
if ($_SESSION['role'] === 'client') {
|
||||
$_SESSION['client_id'] = (int) $c['reference_id'];
|
||||
header('Location: /public/client/informations.php');
|
||||
exit;
|
||||
}
|
||||
if ($_SESSION['role'] === 'praticien') {
|
||||
$_SESSION['praticien_id'] = (int) $c['reference_id'];
|
||||
header('Location: /public/praticien/informations.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// fallback si role inconnu
|
||||
header('Location: /connexion.php');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
if ($_SESSION['role'] === 'client') {
|
||||
$_SESSION['client_id'] = (int) $compte['reference_id'];
|
||||
header('Location: /public/client/informations.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SESSION['role'] === 'praticien') {
|
||||
$_SESSION['praticien_id'] = (int) $compte['reference_id'];
|
||||
header('Location: /public/praticien/informations.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
header('Location: /connexion.php');
|
||||
exit;
|
||||
// =====================
|
||||
// 5) Écran final (succès)
|
||||
// =====================
|
||||
|
||||
@@ -1,271 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/../includes/bootstrap_global.php';
|
||||
|
||||
/**
|
||||
* UI helper: affiche une jolie carte centrée puis exit.
|
||||
* $type: 'error' | 'info' | 'success'
|
||||
*/
|
||||
function ui_exit(string $title, string $message, int $status = 400, string $type = 'error', ?string $cta_href = '/', ?string $cta_label = null): void {
|
||||
http_response_code($status);
|
||||
$title = htmlspecialchars($title, ENT_QUOTES, 'UTF-8');
|
||||
$message = nl2br(htmlspecialchars($message, ENT_QUOTES, 'UTF-8'));
|
||||
$cta_href = $cta_href ? htmlspecialchars($cta_href, ENT_QUOTES, 'UTF-8') : null;
|
||||
$cta_label = $cta_label ?? 'OK';
|
||||
|
||||
// Styles d'icône selon le type
|
||||
$iconBorder = '#B91C1C'; // rouge par défaut (error)
|
||||
$iconBg = 'rgba(185,28,28,.08)';
|
||||
$iconRing = 'rgba(185,28,28,.25)';
|
||||
$checkCSS = ''; // pour success
|
||||
if ($type === 'info') {
|
||||
$iconBorder = '#1D4ED8';
|
||||
$iconBg = 'rgba(29,78,216,.08)';
|
||||
$iconRing = 'rgba(29,78,216,.25)';
|
||||
} elseif ($type === 'success') {
|
||||
$iconBorder = '#1E8050';
|
||||
$iconBg = 'rgba(30,128,80,.08)';
|
||||
$iconRing = 'rgba(30,128,80,.20)';
|
||||
$checkCSS = 'width:48px;height:36px;border:5px solid #1e8050;border-top:0;border-left:0;transform:rotate(45deg);';
|
||||
}
|
||||
|
||||
echo '<!DOCTYPE html><html lang="fr"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">';
|
||||
echo '<title>'.$title.'</title>';
|
||||
echo '<style>
|
||||
html,body{height:100%;margin:0}
|
||||
body{display:flex;align-items:center;justify-content:center;background:#8CA9FF;font-family:Montserrat,system-ui,Segoe UI,Roboto,Arial}
|
||||
.card{max-width:720px;width:94%;background:#fff;border:1px solid #e5e7eb;border-radius:14px;box-shadow:0 8px 24px rgba(0,0,0,.08);padding:28px;text-align:center}
|
||||
h1{margin:6px 0 10px;font-size:clamp(22px,2.4vw,28px)}
|
||||
p{margin:0 0 18px;color:#6b7280;font-size:clamp(16px,2.2vw,18px)}
|
||||
.row{display:flex;gap:10px;justify-content:center;flex-wrap:wrap;margin-top:8px}
|
||||
.btn{padding:10px 14px;border-radius:10px;text-decoration:none;display:inline-block}
|
||||
.btn-primary{background:#2563eb;color:#fff}
|
||||
.btn-outline{border:1px solid #d1d5db;color:#374151;background:#fff}
|
||||
</style></head><body><div class="card">';
|
||||
|
||||
// Icône
|
||||
echo '<div style="width:102px;height:102px;border-radius:50%;background:'.$iconBg.';border:3px solid '.$iconRing.';display:flex;align-items:center;justify-content:center;margin:0 auto 20px;">';
|
||||
if ($type === 'success') {
|
||||
echo '<div style="'.$checkCSS.'"></div>';
|
||||
} elseif ($type === 'info') {
|
||||
echo '<div style="width:14px;height:48px;border-left:5px solid '.$iconBorder.';position:relative;">
|
||||
<div style="position:absolute;top:-18px;left:-5px;width:10px;height:10px;background:'.$iconBorder.';border-radius:50%;"></div>
|
||||
</div>';
|
||||
} else { // error
|
||||
echo '<div style="position:relative;width:48px;height:48px;">
|
||||
<div style="position:absolute;top:0;left:22px;width:5px;height:48px;background:'.$iconBorder.';transform:rotate(45deg);border-radius:2px;"></div>
|
||||
<div style="position:absolute;top:0;left:22px;width:5px;height:48px;background:'.$iconBorder.';transform:rotate(-45deg);border-radius:2px;"></div>
|
||||
</div>';
|
||||
}
|
||||
echo '</div>';
|
||||
|
||||
echo '<h1>'.$title.'</h1>';
|
||||
echo '<p>'.$message.'</p>';
|
||||
|
||||
if ($cta_href) {
|
||||
echo '<div class="row"><a class="btn btn-outline" href="'.$cta_href.'">'.$cta_label.'</a></div>';
|
||||
}
|
||||
|
||||
echo '</div></body></html>';
|
||||
exit;
|
||||
}
|
||||
|
||||
// =====================
|
||||
// 1) Token présent & valide
|
||||
// =====================
|
||||
$rawToken = trim((string)($_GET['token'] ?? ''));
|
||||
$rawAL = trim((string)($_GET['al'] ?? ''));
|
||||
if ($rawAL !== '' && !preg_match('/^[A-Fa-f0-9]{64}$/', $rawAL)) {
|
||||
$rawAL = ''; // on ignore si format suspect
|
||||
}
|
||||
|
||||
if ($rawToken === '') {
|
||||
ui_exit(
|
||||
$translations['err_titre'] ?? 'Un problème est survenu',
|
||||
$translations['err_lien_invalide'] ?? 'Lien invalide.',
|
||||
400,
|
||||
'error',
|
||||
'/',
|
||||
$translations['retour_acceuil'] ?? 'Retour à l’accueil'
|
||||
);
|
||||
}
|
||||
// Optionnel : on vérifie le format attendu (bin2hex(32) => 64 hexdigits)
|
||||
if (!preg_match('/^[A-Fa-f0-9]{64}$/', $rawToken)) {
|
||||
ui_exit(
|
||||
$translations['err_titre'] ?? 'Un problème est survenu',
|
||||
$translations['err_lien_invalide'] ?? 'Lien invalide.',
|
||||
400,
|
||||
'error',
|
||||
'/',
|
||||
$translations['retour_acceuil'] ?? 'Retour à l’accueil'
|
||||
);
|
||||
}
|
||||
|
||||
// =====================
|
||||
// 2) Récupération du compte
|
||||
// =====================
|
||||
$stmt = $pdo->prepare("SELECT * FROM compte WHERE token_validation = :token LIMIT 1");
|
||||
$stmt->execute(['token' => $rawToken]);
|
||||
$compte = $stmt->fetch();
|
||||
|
||||
if (!$compte) {
|
||||
ui_exit(
|
||||
$translations['err_titre'] ?? 'Un problème est survenu',
|
||||
$translations['err_token_invalide'] ?? 'Token invalide.',
|
||||
400,
|
||||
'error',
|
||||
'/',
|
||||
$translations['retour_acceuil'] ?? 'Retour à l’accueil'
|
||||
);
|
||||
}
|
||||
|
||||
// =====================
|
||||
// 3) Validité temporelle (6h)
|
||||
// =====================
|
||||
try {
|
||||
$created_at = new DateTime($compte['cree_le']);
|
||||
} catch (Throwable $e) {
|
||||
// Par prudence : si la date est illisible, on refuse
|
||||
ui_exit(
|
||||
$translations['err_titre'] ?? 'Un problème est survenu',
|
||||
$translations['err_lien_invalide'] ?? 'Lien invalide.',
|
||||
400,
|
||||
'error',
|
||||
'/',
|
||||
$translations['retour_acceuil'] ?? 'Retour à l’accueil'
|
||||
);
|
||||
}
|
||||
$now = new DateTime();
|
||||
$diffH = ($now->getTimestamp() - $created_at->getTimestamp()) / 3600;
|
||||
|
||||
if ($diffH > 6) {
|
||||
// Supprimer les enregistrements expirés
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
|
||||
$pdo->prepare("DELETE FROM compte WHERE id = :id")->execute(['id' => $compte['id']]);
|
||||
|
||||
$ref_id = (int)$compte['reference_id'];
|
||||
if (($compte['role'] ?? '') === 'praticien') {
|
||||
$pdo->prepare("DELETE FROM praticien WHERE id = :id")->execute(['id' => $ref_id]);
|
||||
} else {
|
||||
$pdo->prepare("DELETE FROM client WHERE id = :id")->execute(['id' => $ref_id]);
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
} catch (Throwable $e) {
|
||||
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||
error_log('Validation email - purge expirée échouée: '.$e->getMessage());
|
||||
ui_exit(
|
||||
$translations['err_titre'] ?? 'Un problème est survenu',
|
||||
$translations['err_serveur'] ?? 'Erreur serveur.',
|
||||
500,
|
||||
'error',
|
||||
'/',
|
||||
$translations['retour_acceuil'] ?? 'Retour à l’accueil'
|
||||
);
|
||||
}
|
||||
|
||||
ui_exit(
|
||||
$translations['err_titre'] ?? 'Un problème est survenu',
|
||||
$translations['err_lien_expire'] ?? 'Le lien de validation a expiré.',
|
||||
410,
|
||||
'info',
|
||||
'/',
|
||||
$translations['retour_acceuil'] ?? 'Retour à l’accueil'
|
||||
);
|
||||
}
|
||||
|
||||
// =====================
|
||||
// 4) Valider l'email (maj compte)
|
||||
// =====================
|
||||
try {
|
||||
$stmt = $pdo->prepare("UPDATE compte SET email_valide = 1, token_validation = NULL WHERE id = :id");
|
||||
$stmt->execute(['id' => $compte['id']]);
|
||||
} catch (Throwable $e) {
|
||||
error_log('Validation email - UPDATE échouée: '.$e->getMessage());
|
||||
ui_exit(
|
||||
$translations['err_titre'] ?? 'Un problème est survenu',
|
||||
$translations['err_serveur'] ?? 'Erreur serveur.',
|
||||
500,
|
||||
'error',
|
||||
'/',
|
||||
$translations['retour_acceuil'] ?? 'Retour à l’accueil'
|
||||
);
|
||||
}
|
||||
|
||||
// =====================
|
||||
// 4bis) Auto-login (si al fourni et valide)
|
||||
// =====================
|
||||
if ($rawAL !== '') {
|
||||
// On recharge le compte pour lire token_autologin + expire (plus propre)
|
||||
$stmt = $pdo->prepare("SELECT id, role, reference_id, token_autologin, token_autologin_expire
|
||||
FROM compte
|
||||
WHERE id = :id
|
||||
LIMIT 1");
|
||||
$stmt->execute(['id' => (int)$compte['id']]);
|
||||
$c = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($c && !empty($c['token_autologin']) && hash_equals((string)$c['token_autologin'], $rawAL)) {
|
||||
|
||||
// Expiration (obligatoire)
|
||||
$expOk = false;
|
||||
if (!empty($c['token_autologin_expire'])) {
|
||||
try {
|
||||
$exp = new DateTime((string)$c['token_autologin_expire']);
|
||||
$expOk = (new DateTime()) <= $exp;
|
||||
} catch (Throwable $e) {
|
||||
$expOk = false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($expOk) {
|
||||
// One-time : on invalide le token autologin
|
||||
$pdo->prepare("UPDATE compte
|
||||
SET token_autologin = NULL, token_autologin_expire = NULL
|
||||
WHERE id = :id")
|
||||
->execute(['id' => (int)$c['id']]);
|
||||
|
||||
// Connexion session (même logique pour client/praticien)
|
||||
session_regenerate_id(true);
|
||||
$_SESSION['compte_id'] = (int)$c['id']; // <-- IMPORTANT (secure.php l'exige)
|
||||
$_SESSION['role'] = (string)$c['role'];
|
||||
$_SESSION['reference_id'] = (int)$c['reference_id'];
|
||||
|
||||
if ($_SESSION['role'] === 'client') {
|
||||
$_SESSION['client_id'] = (int)$c['reference_id'];
|
||||
header('Location: /public/client/informations.php');
|
||||
exit;
|
||||
}
|
||||
if ($_SESSION['role'] === 'praticien') {
|
||||
$_SESSION['praticien_id'] = (int)$c['reference_id'];
|
||||
header('Location: /public/praticien/informations.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// fallback si role inconnu
|
||||
header('Location: /connexion.php');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// =====================
|
||||
// 5) Écran final (succès)
|
||||
// =====================
|
||||
// Le texte de succès chez toi contient un lien HTML ; on affiche un message “texte” + un vrai bouton vers la connexion.
|
||||
$successMsg = isset($translations['success_validation_texte'])
|
||||
? strip_tags((string)$translations['success_validation_texte'])
|
||||
: 'Votre compte est confirmé. Vous pouvez maintenant vous connecter.';
|
||||
|
||||
ui_exit(
|
||||
$translations['success_validation_titre'] ?? 'Compte confirmé !',
|
||||
$successMsg,
|
||||
200,
|
||||
'success',
|
||||
'/connexion.php',
|
||||
$translations['btn_se_connecter'] ?? 'Se connecter'
|
||||
);
|
||||
|
||||
|
||||
Reference in new issue
Block a user