Version stable, aprelic, connexion CFRS et autres corrections
This commit is contained in:
1 parent
865de8a976
commit
e9d0010451
62 files changed
+12851
-2624
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');
|
||||
}
|
||||
+54
-25
@@ -1,50 +1,73 @@
|
||||
<?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'] ?? '';
|
||||
|
||||
// 🔒 --- 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 {
|
||||
|
||||
if ($email === '' || $mot_de_passe === '') {
|
||||
$erreur = $translations['erreur_champs_vides'];
|
||||
} 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'])) {
|
||||
|
||||
// ✅ 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, // 15 min
|
||||
'expires_at' => time() + 900,
|
||||
];
|
||||
|
||||
$erreur = $translations['tvalidat_erreur_email_non_valide'] ?? $translations['erreur_email_non_valide'] ?? 'Veuillez valider votre adresse email avant de vous connecter.';
|
||||
$erreur = $translations['tvalidat_erreur_email_non_valide'] ?? $translations['erreur_email_non_valide'] ?? 'Veuillez valider votre adresse email.';
|
||||
|
||||
} else {
|
||||
session_regenerate_id(true); // ✅ anti session fixation
|
||||
// 2) Connexion OK → on initialise la session
|
||||
|
||||
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();
|
||||
|
||||
// 3) On gère la redirection prioritaire vers la page demandée
|
||||
// 🔁 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;
|
||||
}
|
||||
|
||||
// 4) Sinon, fallback par rôle
|
||||
// 🎯 fallback rôle
|
||||
if ($_SESSION['role'] === 'client') {
|
||||
header('Location: /public/client/mes_rendez_vous.php');exit;
|
||||
} elseif ($_SESSION['role'] === 'admin') {
|
||||
@@ -55,11 +78,25 @@
|
||||
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'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
@@ -69,13 +106,9 @@
|
||||
<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="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') ?>">
|
||||
@@ -85,25 +118,21 @@
|
||||
<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'; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="header-actions">
|
||||
<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="/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>
|
||||
@@ -114,7 +143,6 @@
|
||||
</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 text-dark"><?php echo $translations['connexion_titre'] ?></h3>
|
||||
|
||||
@@ -153,6 +181,7 @@
|
||||
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'] ?>"
|
||||
@@ -170,11 +199,11 @@
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- on propage le "next" -->
|
||||
<input type="hidden" name="next"
|
||||
value="<?php echo htmlspecialchars($_GET['next'] ?? ($_SESSION['redirect_to'] ?? ''), ENT_QUOTES) ?>">
|
||||
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>
|
||||
@@ -188,26 +217,26 @@
|
||||
<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;
|
||||
|
||||
// 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'); });
|
||||
|
||||
// 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>
|
||||
</body>
|
||||
</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>
|
||||
+263
-67
@@ -1,46 +1,222 @@
|
||||
<?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] ?? [];
|
||||
$currentLang = $lang ?? 'ru';
|
||||
if (! in_array($currentLang, ['ru', 'fr', 'en'], true)) {
|
||||
$currentLang = 'ru';
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ------- (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);
|
||||
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);
|
||||
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"];
|
||||
$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"];
|
||||
$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]; }
|
||||
|
||||
$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; }
|
||||
// ID praticien
|
||||
$praticien_id = (int) ($_GET['id'] ?? 0);
|
||||
if ($praticien_id <= 0) {
|
||||
echo "<div class='container py-5'>ID invalide.</div>";
|
||||
exit;
|
||||
}
|
||||
|
||||
$name = $name = trim($p['prenom'] . ' ' . $p['nom']);
|
||||
$photo = !empty($p['photo']) ? "/public/uploads/".ltrim($p['photo'],'/') : "/public/uploads/img/user_placeholder.png";
|
||||
// 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");
|
||||
@@ -52,30 +228,39 @@ $avg = $pdo->prepare("SELECT ROUND(AVG(note),1) AS m, COUNT(*) AS c FROM avis_pr
|
||||
$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");
|
||||
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">
|
||||
@@ -112,10 +297,10 @@ else: ?>
|
||||
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,26 +315,35 @@ 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">
|
||||
<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'] != '') {?>
|
||||
<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');
|
||||
@@ -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']); ?>
|
||||
<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;
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
<?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',
|
||||
@@ -43,9 +44,68 @@ $UGLY_TO_PRETTY = [
|
||||
];
|
||||
|
||||
// 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));
|
||||
}
|
||||
+367
-27
@@ -1,65 +1,405 @@
|
||||
<?php
|
||||
function has_cyrillic(string $s): bool {
|
||||
|
||||
function has_cyrillic(string $s): bool
|
||||
{
|
||||
return (bool) preg_match('/\p{Cyrillic}/u', $s);
|
||||
}
|
||||
|
||||
function norm_key(string $s): string {
|
||||
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
|
||||
$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 {
|
||||
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);
|
||||
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',
|
||||
'а' => '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 {
|
||||
// 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 [];
|
||||
if ($q === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$aliases = @include __DIR__ . '/city_aliases_ru.php';
|
||||
if (! is_array($aliases)) {
|
||||
$aliases = [];
|
||||
}
|
||||
|
||||
$cands = [];
|
||||
$cands[] = mb_strtolower($q, 'UTF-8'); // saisie brute (si déjà latine)
|
||||
$qLower = mb_strtolower($q, 'UTF-8');
|
||||
|
||||
// charger alias RU -> latin
|
||||
$aliases = @include __DIR__ . '/city_aliases_ru.php';
|
||||
if (!is_array($aliases)) $aliases = [];
|
||||
// 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)) {
|
||||
// 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;
|
||||
$cands[] = mb_strtolower($lat, 'UTF-8');
|
||||
$cands[] = mb_strtolower($ru, 'UTF-8');
|
||||
}
|
||||
}
|
||||
// 2) translittération simple (Париж -> parizh)
|
||||
|
||||
$cands[] = translit_ru_to_lat($q);
|
||||
}
|
||||
|
||||
// Normalisation légère des candidats (minuscules, accents retirés si possible)
|
||||
// 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) {
|
||||
$lc = mb_strtolower($c, 'UTF-8');
|
||||
// enlever accents (si intl transliterator dispo)
|
||||
$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;');
|
||||
if ($t) $lc = $t->transliterate($lc);
|
||||
$t = \Transliterator::create('Any-Latin; Latin-ASCII; Lower();');
|
||||
if ($t) {
|
||||
$s = $t->transliterate($s);
|
||||
}
|
||||
$res[] = $lc;
|
||||
}
|
||||
return array_values(array_unique($res));
|
||||
|
||||
$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;
|
||||
}
|
||||
@@ -13,7 +13,6 @@ return [
|
||||
'gid' => 'гид',
|
||||
'rieltori' => 'риелтор',
|
||||
|
||||
|
||||
],
|
||||
'fr' => [
|
||||
'avocats' => 'адвокат',
|
||||
|
||||
@@ -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>
|
||||
+44
-44
@@ -3,31 +3,30 @@ declare(strict_types=1);
|
||||
require_once __DIR__ . '/../includes/bootstrap_global.php';
|
||||
|
||||
$message = '';
|
||||
$messageType = '';
|
||||
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
|
||||
$email = trim($_POST['email'] ?? '');
|
||||
|
||||
// Email valide + pas d’injection d’en-têtes
|
||||
// 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['err_email_non_valide'] ?? 'Email invalide.';
|
||||
$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_code'] = (string) $code;
|
||||
$_SESSION['reset_time'] = time();
|
||||
$_SESSION['reset_ttl'] = 600; // 10 min
|
||||
$_SESSION['reset_ttl'] = 600;
|
||||
$_SESSION['reset_attempts'] = 0;
|
||||
|
||||
// Prépare l'e-mail
|
||||
$sujet = $translations['mail_sujet_code'];
|
||||
$sujet = $translations['mail_sujet_code'] ?? 'Code de réinitialisation';
|
||||
$codeHtml = htmlspecialchars((string) $code, ENT_QUOTES, 'UTF-8');
|
||||
|
||||
$html = "<!DOCTYPE html>
|
||||
@@ -44,36 +43,38 @@ 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='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>".$translations['mail_phrase_validite']."</p>
|
||||
<div class='footer'>".$translations['mail_signature']."</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".
|
||||
($translations['mail_bonjour'] ?? 'Bonjour') . "\n\n" .
|
||||
($translations['mail_phrase_code'] ?? 'Voici votre code de réinitialisation :') . "\n" .
|
||||
$code . "\n" .
|
||||
$translations['mail_phrase_validite']."\n\n".
|
||||
$translations['mail_signature']
|
||||
($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) {
|
||||
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';
|
||||
}
|
||||
}
|
||||
?>
|
||||
@@ -81,13 +82,12 @@ if (($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
|
||||
<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>
|
||||
@@ -95,13 +95,8 @@ if (($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
|
||||
<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'; ?>
|
||||
@@ -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>
|
||||
<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>
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
<?php
|
||||
// /public/cabinet/disponibilites.php (VERSION SECRÉTAIRE)
|
||||
declare (strict_types = 1);
|
||||
|
||||
// /public/cabinet/disponibilites.php
|
||||
require_once __DIR__ . '/../includes/init_secretaire.php';
|
||||
|
||||
if (! function_exists('h')) {
|
||||
function h($s): string
|
||||
{
|
||||
return htmlspecialchars((string) $s, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
}
|
||||
|
||||
// --- Helpers
|
||||
if (!function_exists('h')) { function h($s){ 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'); }
|
||||
$praticien_id = (int) ($_GET['praticien_id'] ?? 0);
|
||||
|
||||
// --- Vérifier pivot + droit 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
|
||||
@@ -21,103 +29,156 @@ $st = $pdo->prepare("
|
||||
AND is_active = 1
|
||||
LIMIT 1
|
||||
");
|
||||
$st->execute(['u'=>$compteId,'p'=>$praticien_id]);
|
||||
$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'); }
|
||||
|
||||
// ---- Ajouter un créneau (POST)
|
||||
if (! $pivot || (int) $pivot['can_edit_slots'] !== 1) {
|
||||
http_response_code(403);
|
||||
exit('Forbidden');
|
||||
}
|
||||
|
||||
// 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;
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['jour'], $_POST['heure_debut'], $_POST['heure_fin'])) {
|
||||
|
||||
$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']);
|
||||
|
||||
// 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) {
|
||||
$erreur = $translations['erreur_debut_avant_fin'] ?? "Heure de début après l’heure de fin.";
|
||||
}
|
||||
$md = $toMin($debut);
|
||||
$mf = $toMin($fin);
|
||||
|
||||
if (!$erreur) {
|
||||
$stmt = $pdo->prepare(
|
||||
"INSERT INTO disponibilite (praticien_id, jour_semaine, heure_debut, heure_fin)
|
||||
VALUES (:p, :j, :hd, :hf)"
|
||||
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.";
|
||||
} else {
|
||||
$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
|
||||
'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');
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Récupérer les créneaux
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT * FROM disponibilite
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT *
|
||||
FROM disponibilite
|
||||
WHERE praticien_id = ?
|
||||
ORDER BY FIELD(jour_semaine,'lundi','mardi','mercredi','jeudi','vendredi','samedi','dimanche'), heure_debut"
|
||||
);
|
||||
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
|
||||
$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',
|
||||
];
|
||||
$PAGE_TITRE = $translations['titre_disponibilites_date'] ?? 'Disponibilités (dates)';
|
||||
|
||||
$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';
|
||||
?>
|
||||
|
||||
<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>
|
||||
<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=<?= (int)$praticien_id ?>" class="btn btn-outline-secondary mb-4">
|
||||
<?= htmlspecialchars($translations['btn_ajouter_par_date'] ?? 'Ajouter par date') ?>
|
||||
<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><?= 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>
|
||||
<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): foreach ($dispos as $d): ?>
|
||||
<?php if ($dispos): ?>
|
||||
<?php foreach ($dispos as $d): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<?php
|
||||
@@ -125,31 +186,38 @@ require_once __DIR__ . '/../includes/header_cabinet.php';
|
||||
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><?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"
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-danger js-del"
|
||||
data-id="<?= (int)$d['id'] ?>">
|
||||
<?= h($translations['btn_supprimer'] ?? 'Supprimer') ?>
|
||||
data-id="<?php echo (int) $d['id'] ?>">
|
||||
<?php echo 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 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>
|
||||
<h4 class="mb-3 text-center"><?php echo h($translations['card_ajouter_creneau'] ?? 'Ajouter un créneau') ?></h4>
|
||||
|
||||
<form method="POST">
|
||||
<input type="hidden" name="praticien_id" value="<?= (int)$praticien_id ?>">
|
||||
<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="jour"><?= h($translations['label_jour'] ?? 'Jour') ?></label>
|
||||
<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=""><?= h($translations['option_choisir'] ?? '— Choisir —') ?></option>
|
||||
<option value=""><?php echo h($translations['option_choisir'] ?? '— Choisir —') ?></option>
|
||||
<?php
|
||||
$jours = [
|
||||
'lundi' => $translations['jour_lundi'] ?? 'Lundi',
|
||||
@@ -162,23 +230,23 @@ require_once __DIR__ . '/../includes/header_cabinet.php';
|
||||
];
|
||||
foreach ($jours as $val => $label):
|
||||
?>
|
||||
<option value="<?= h($val) ?>"><?= h($label) ?></option>
|
||||
<option value="<?php echo h($val) ?>"><?php echo h($label) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="heure_debut"><?= h($translations['label_heure_debut'] ?? 'Heure de début') ?></label>
|
||||
<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"><?= h($translations['label_heure_fin'] ?? 'Heure de fin') ?></label>
|
||||
<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">
|
||||
<?= h($translations['btn_ajouter'] ?? 'Ajouter') ?>
|
||||
<?php echo h($translations['btn_ajouter'] ?? 'Ajouter') ?>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -187,62 +255,104 @@ require_once __DIR__ . '/../includes/header_cabinet.php';
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// --- SweetAlert2: messages succès / erreur depuis PHP ---
|
||||
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é.") ?>',
|
||||
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) {
|
||||
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,14 +365,21 @@ 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>
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
<?php
|
||||
// /public/cabinet/disponibilites_date.php (VERSION SECRÉTAIRE)
|
||||
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'); }
|
||||
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;
|
||||
$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("
|
||||
@@ -23,20 +30,39 @@ $st = $pdo->prepare("
|
||||
AND is_active = 1
|
||||
LIMIT 1
|
||||
");
|
||||
$st->execute(['u'=>$compteId,'p'=>$praticien_id]);
|
||||
$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'); }
|
||||
|
||||
// --- Optionnel: récupérer nom praticien pour titre
|
||||
if (! $pivot || (int) $pivot['can_edit_slots'] !== 1) {
|
||||
http_response_code(403);
|
||||
exit('Forbidden');
|
||||
}
|
||||
|
||||
// --- Nom praticien
|
||||
$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 (! $prat) {
|
||||
http_response_code(404);
|
||||
exit('Praticien introuvable');
|
||||
}
|
||||
|
||||
$currentPraticienName = trim(((string) ($prat['prenom'] ?? '')) . ' ' . ((string) ($prat['nom'] ?? '')));
|
||||
|
||||
$erreur = null;
|
||||
|
||||
// ---- Ajouter une plage datée (POST)
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['date_dispo'], $_POST['heure_debut'], $_POST['heure_fin'])) {
|
||||
// ---- 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
|
||||
@@ -75,24 +101,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['date_dispo'], $_POST[
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 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
|
||||
@@ -103,30 +111,28 @@ $stmt = $pdo->prepare("
|
||||
$stmt->execute([$praticien_id]);
|
||||
$dates = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Header cabinet
|
||||
|
||||
|
||||
|
||||
$PAGE_TITRE = $translations['titre_disponibilites_date'] ?? 'Disponibilités (dates)';
|
||||
$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';
|
||||
?>
|
||||
|
||||
<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 echo h($currentPraticienName ?: ($translations['h2_disponibilites_date'] ?? 'Disponibilités ponctuelles')) ?>
|
||||
<?php if ($currentPraticienName): ?>
|
||||
- <?= h($translations['h2_disponibilites_date'] ?? 'Disponibilités ponctuelles') ?>
|
||||
- <?php echo 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') ?>
|
||||
href="/public/cabinet/disponibilites.php?praticien_id=<?php echo (int) $praticien_id ?>">
|
||||
<?php echo h($translations['retour_au_planning'] ?? 'Retour') ?>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -134,53 +140,59 @@ require_once __DIR__ . '/../includes/header_cabinet.php';
|
||||
<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>
|
||||
<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): foreach ($dates as $d): ?>
|
||||
<?php if ($dates): ?>
|
||||
<?php 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><?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="<?= (int)$d['id'] ?>">
|
||||
<?= h($translations['btn_supprimer'] ?? 'Supprimer') ?>
|
||||
<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; else: ?>
|
||||
<tr><td colspan="4"><?= h($translations['aucun_creneau'] ?? 'Aucun créneau.') ?></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_date'] ?? 'Ajouter une disponibilité ponctuelle') ?></h4>
|
||||
<h4 class="mb-3 text-center"><?php echo h($translations['card_ajouter_creneau_date'] ?? 'Ajouter une disponibilité ponctuelle') ?></h4>
|
||||
|
||||
<form method="POST">
|
||||
<input type="hidden" name="praticien_id" value="<?= (int)$praticien_id ?>">
|
||||
<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="date_dispo"><?= h($translations['cl_date_rdv'] ?? 'Date') ?></label>
|
||||
<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"><?= h($translations['label_heure_debut'] ?? 'Heure de début') ?></label>
|
||||
<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"><?= h($translations['label_heure_fin'] ?? 'Heure de fin') ?></label>
|
||||
<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">
|
||||
<?= h($translations['btn_ajouter'] ?? 'Ajouter') ?>
|
||||
<?php echo h($translations['btn_ajouter'] ?? 'Ajouter') ?>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -189,40 +201,101 @@ require_once __DIR__ . '/../includes/header_cabinet.php';
|
||||
</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,14 +311,19 @@ 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>
|
||||
|
||||
@@ -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');
|
||||
|
||||
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 = $_SESSION['reference_id'];
|
||||
$praticien_id = intval($_POST['praticien_id'] ?? 0);
|
||||
$client_id = (int) ($_SESSION['reference_id'] ?? 0);
|
||||
$praticien_id = (int) ($_POST['praticien_id'] ?? 0);
|
||||
|
||||
if ($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 * FROM praticien_client WHERE client_id = ? AND praticien_id = ?");
|
||||
$check = $pdo->prepare("
|
||||
SELECT 1
|
||||
FROM praticien_client
|
||||
WHERE client_id = ? AND praticien_id = ?
|
||||
LIMIT 1
|
||||
");
|
||||
$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 ($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;
|
||||
}
|
||||
@@ -1,17 +1,15 @@
|
||||
<?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 {
|
||||
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;
|
||||
@@ -49,12 +47,16 @@ function send_email_phpmailer(
|
||||
$mail->Debugoutput = function ($str, $lvl) {error_log("SMTP[$lvl] " . $str);};
|
||||
}
|
||||
|
||||
if (!empty($MAILCFG['bounce'])) $mail->Sender = $MAILCFG['bounce'];
|
||||
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] ?? '');
|
||||
if (! empty($rpto[0])) {
|
||||
$mail->addReplyTo($rpto[0], $rpto[1] ?? '');
|
||||
}
|
||||
|
||||
$mail->addAddress($toEmail);
|
||||
|
||||
@@ -82,7 +84,10 @@ function send_email_phpmailer(
|
||||
|
||||
} catch (Throwable $e) {
|
||||
$err = 'PHPMailer error: ' . $e->getMessage();
|
||||
if (!empty($mail->ErrorInfo)) $err .= ' | ErrorInfo: '.$mail->ErrorInfo;
|
||||
if (! empty($mail->ErrorInfo)) {
|
||||
$err .= ' | ErrorInfo: ' . $mail->ErrorInfo;
|
||||
}
|
||||
|
||||
error_log($err);
|
||||
|
||||
// Fallback : réessaye SANS ICS
|
||||
@@ -94,7 +99,10 @@ function send_email_phpmailer(
|
||||
}
|
||||
} catch (Throwable $e2) {
|
||||
$err2 = 'PHPMailer fallback error: ' . $e2->getMessage();
|
||||
if (!empty($mail->ErrorInfo)) $err2 .= ' | ErrorInfo: '.$mail->ErrorInfo;
|
||||
if (! empty($mail->ErrorInfo)) {
|
||||
$err2 .= ' | ErrorInfo: ' . $mail->ErrorInfo;
|
||||
}
|
||||
|
||||
error_log($err2);
|
||||
}
|
||||
return [false, $err];
|
||||
@@ -102,7 +110,8 @@ function send_email_phpmailer(
|
||||
}
|
||||
|
||||
// --------- Helpers e-mail (HTML/Texte) ----------
|
||||
function email_html_cancel(array $data, array $t): string {
|
||||
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>
|
||||
@@ -127,20 +136,26 @@ function email_html_cancel(array $data, array $t): string {
|
||||
</td></tr>
|
||||
</table></body></html>';
|
||||
}
|
||||
function email_text_cancel(array $data, array $t): string {
|
||||
function email_text_cancel(array $data, array $t): string
|
||||
{
|
||||
$lines = [];
|
||||
$lines[] = $data['hello_line']; $lines[]='';
|
||||
$lines[] = $data['intro']; $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'];
|
||||
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 {
|
||||
function ics_escape(string $s): string
|
||||
{
|
||||
$s = str_replace("\\", "\\\\", $s);
|
||||
$s = str_replace(";", "\;", $s);
|
||||
$s = str_replace(",", "\,", $s);
|
||||
@@ -166,11 +181,20 @@ function build_ics_cancel(
|
||||
$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');
|
||||
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;
|
||||
if (empty($a['email'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$cn = ics_escape($a['cn'] ?? '');
|
||||
$lines[] = "ATTENDEE;ROLE=REQ-PARTICIPANT;CN={$cn}:mailto:" . $a['email'];
|
||||
}
|
||||
@@ -294,7 +318,7 @@ try {
|
||||
'/public/client/mes_rendez_vous.php',
|
||||
[
|
||||
'rdv_id' => (int) $id,
|
||||
'action' => 'cancel'
|
||||
'action' => 'cancel',
|
||||
]
|
||||
);
|
||||
|
||||
@@ -302,7 +326,6 @@ try {
|
||||
error_log('push rdv cancel (client) failed: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
|
||||
// 2) Infos RDV pour e-mails
|
||||
$info = $pdo->prepare("
|
||||
SELECT rv.date_rdv, rv.duree_minutes, rv.uuid_ical,
|
||||
@@ -357,7 +380,8 @@ try {
|
||||
$text_p = email_text_cancel($data_p, $translations);
|
||||
$html_p = email_html_cancel($data_p, $translations);
|
||||
|
||||
$ics_p = null; $ics_name_p = null;
|
||||
$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(
|
||||
@@ -371,7 +395,9 @@ try {
|
||||
);
|
||||
}
|
||||
[$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);
|
||||
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';
|
||||
@@ -397,7 +423,8 @@ try {
|
||||
$text_c = email_text_cancel($data_c, $translations);
|
||||
$html_c = email_html_cancel($data_c, $translations);
|
||||
|
||||
$ics_c = null; $ics_name_c = null;
|
||||
$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(
|
||||
@@ -411,7 +438,9 @@ try {
|
||||
);
|
||||
}
|
||||
[$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);
|
||||
if (! $ok_c) {
|
||||
error_log("Annulation RDV: mail client NON envoyé: " . $err_c);
|
||||
}
|
||||
|
||||
json_out(true, $translations['rdv_annule_ok'] ?? 'Rendez-vous annulé.');
|
||||
|
||||
|
||||
+45
-28
@@ -1,36 +1,40 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../includes/init_client.php';
|
||||
|
||||
|
||||
|
||||
$success = $erreur = '';
|
||||
$id = $_SESSION['compte_id'];
|
||||
|
||||
$stmt = $pdo->prepare("SELECT email FROM compte WHERE id = ?");
|
||||
$stmt = $pdo->prepare("SELECT email, mot_de_passe FROM compte WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$compte = $stmt->fetch();
|
||||
|
||||
if (!$compte) die($translations['compte_introuvable']);
|
||||
if (! $compte) {
|
||||
die($translations['compte_introuvable']);
|
||||
}
|
||||
|
||||
$email_actuel = $compte['email'];
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
require_post_csrf();
|
||||
|
||||
$nouvel_email = trim($_POST['email'] ?? '');
|
||||
$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,7 +43,12 @@ 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'];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,23 +60,31 @@ require_once '../includes/header.php';
|
||||
|
||||
|
||||
<?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"><?= $translations['label_nouveau_mdp_facultatif'] ?></label>
|
||||
<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" id="mot_de_passe" placeholder="<?= $translations['nouveau_mdp_placeholder'] ?>" class="form-control">
|
||||
<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"><?php echo $translations['label_nouveau_mdp_facultatif'] ?></label>
|
||||
<div class="field-wrapper">
|
||||
<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>
|
||||
|
||||
@@ -1,21 +1,33 @@
|
||||
<?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;}
|
||||
|
||||
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);
|
||||
$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; $u = trim($url);
|
||||
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"];
|
||||
}
|
||||
@@ -28,13 +40,43 @@ function video_thumb_info(?string $url): ?array {
|
||||
}
|
||||
|
||||
// 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]);
|
||||
$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']);
|
||||
$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
|
||||
@@ -63,26 +105,26 @@ 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'] != '') {?>
|
||||
<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');
|
||||
@@ -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']); ?>
|
||||
<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>
|
||||
|
||||
@@ -7,7 +7,8 @@ $__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; } }
|
||||
foreach ($__paths as $__p) {if (is_file($__p)) {require_once $__p;
|
||||
break;}}
|
||||
unset($__paths, $__p);
|
||||
|
||||
/* =========================
|
||||
@@ -20,12 +21,20 @@ $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;
|
||||
function video_thumb_info(?string $url): ?array
|
||||
{
|
||||
if (! $url || ! trim($url)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$u = trim($url);
|
||||
|
||||
// YouTube
|
||||
@@ -40,20 +49,34 @@ function video_thumb_info(?string $url): ?array {
|
||||
}
|
||||
// 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];
|
||||
if (preg_match('~\\.(mp4|webm|ogg)$~i', $path)) {
|
||||
return ['provider' => 'file', 'id' => null, 'thumb' => null];
|
||||
}
|
||||
|
||||
if (strpos(strtolower($u), 'vimeo.com') !== false) {
|
||||
return ['provider' => 'vimeo', 'id' => null, 'thumb' => null];
|
||||
}
|
||||
|
||||
return ['provider' => 'other', 'id' => null, 'thumb' => null];
|
||||
}
|
||||
|
||||
function fullName(array $p): string {
|
||||
return trim(($p['prenom'] ?? '').' '.($p['nom'] ?? ''));
|
||||
function 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 {
|
||||
function excerpt_words(string $html, int $words = 60): string
|
||||
{
|
||||
$plain = trim(preg_replace('/\\s+/', ' ', strip_tags($html)));
|
||||
if ($plain === '') return '';
|
||||
if ($plain === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$parts = preg_split('/\\s+/', $plain);
|
||||
if (count($parts) <= $words) return $plain;
|
||||
if (count($parts) <= $words) {
|
||||
return $plain;
|
||||
}
|
||||
|
||||
return implode(' ', array_slice($parts, 0, $words)) . '…';
|
||||
}
|
||||
|
||||
@@ -64,7 +87,7 @@ $whereSql = [];
|
||||
$params = [];
|
||||
|
||||
/*==========================BLOCK WHERE AVEC ABONNEMENT VALIDE ==========================*/
|
||||
$whereSql[] = "(date_expiration IS NOT NULL AND date_expiration >= DATE_SUB(NOW(), INTERVAL 14 DAY))";
|
||||
$whereSql[] = "(p.date_expiration IS NOT NULL AND p.date_expiration >= DATE_SUB(NOW(), INTERVAL 14 DAY))";
|
||||
/*=======================================================================================*/
|
||||
|
||||
if ($q !== '') {
|
||||
@@ -76,8 +99,8 @@ if ($q !== '') {
|
||||
$k2 = ":q_spec_$i";
|
||||
|
||||
$and[] = "(
|
||||
LOWER(CONCAT(COALESCE(prenom,''),' ',COALESCE(nom,''))) LIKE $k1
|
||||
OR LOWER(COALESCE(specialite,'')) LIKE $k2
|
||||
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 . '%';
|
||||
@@ -87,7 +110,6 @@ if ($q !== '') {
|
||||
$whereSql[] = '(' . implode(' AND ', $and) . ')';
|
||||
}
|
||||
|
||||
|
||||
if ($w !== '') {
|
||||
// Candidats latins à partir d'une saisie RU (alias + translittération)
|
||||
$villeCands = city_query_candidates($w);
|
||||
@@ -100,12 +122,12 @@ if ($w !== '') {
|
||||
// Ville (LOWER(...)) : on cherche par préfixe
|
||||
foreach ($villeCands as $i => $cand) {
|
||||
$key = ":wv$i";
|
||||
$ors[] = "LOWER(COALESCE(ville,'')) LIKE $key";
|
||||
$ors[] = "LOWER(COALESCE(pt.ville_affichee, p.ville, '')) LIKE $key";
|
||||
$params[$key] = $cand . '%'; // ← passe en '%'.$cand.'%' si tu veux "contient"
|
||||
}
|
||||
|
||||
// Code postal : on garde un LIKE simple sur la saisie brute
|
||||
$ors[] = "COALESCE(code_postal,'') LIKE :wcp";
|
||||
$ors[] = "COALESCE(p.code_postal,'') LIKE :wcp";
|
||||
$params[':wcp'] = '%' . $w . '%';
|
||||
|
||||
$whereSql[] = '(' . implode(' OR ', $ors) . ')';
|
||||
@@ -113,24 +135,54 @@ if ($w !== '') {
|
||||
$whereClause = $whereSql ? (' WHERE ' . implode(' AND ', $whereSql)) : '';
|
||||
|
||||
/* Total pour pagination (utile seulement si !isSearch, mais on calcule une fois) */
|
||||
$sqlCount = "SELECT COUNT(*) FROM praticien".$whereClause;
|
||||
$sqlCount = "
|
||||
SELECT COUNT(*)
|
||||
FROM praticien p
|
||||
LEFT JOIN praticien_traduction pt
|
||||
ON pt.praticien_id = p.id
|
||||
AND pt.langue = :lang
|
||||
$whereClause
|
||||
";
|
||||
|
||||
$stmt = $pdo->prepare($sqlCount);
|
||||
$stmt->execute($params);
|
||||
$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 id, titre, nom, prenom, photo, specialite, ville, code_postal, presentation, video
|
||||
FROM praticien
|
||||
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 specialite ASC, nom ASC
|
||||
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);
|
||||
foreach ($params as $k=>$v) $stmt->bindValue($k, $v, PDO::PARAM_STR);
|
||||
$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);
|
||||
@@ -141,10 +193,11 @@ $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
|
||||
@@ -161,42 +214,42 @@ function render_results(array $praticiens, array $translations, bool $isSearch,
|
||||
<?php else: ?>
|
||||
<?php foreach ($praticiens as $p):
|
||||
$name = fullName($p);
|
||||
$spec = $p['specialite'] ?? '';
|
||||
$ville = $p['ville'] ?? '';
|
||||
$spec = $p['specialite_affichee'] ?? $p['specialite'] ?? '';
|
||||
$ville = $p['ville_affichee'] ?? $p['ville'] ?? '';
|
||||
$cp = $p['code_postal'] ?? '';
|
||||
$present= excerpt_words((string)($p['presentation'] ?? ''), 35);
|
||||
$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>
|
||||
|
||||
@@ -207,12 +260,12 @@ function render_results(array $praticiens, array $translations, bool $isSearch,
|
||||
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>
|
||||
@@ -231,19 +284,19 @@ function render_results(array $praticiens, array $translations, bool $isSearch,
|
||||
|
||||
<?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>
|
||||
<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;
|
||||
}
|
||||
@@ -262,7 +315,7 @@ require_once '../includes/header.php';
|
||||
<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>
|
||||
@@ -349,6 +402,9 @@ require_once '../includes/header.php';
|
||||
|
||||
<!-- 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
|
||||
function toEmbed(url) {
|
||||
@@ -424,35 +480,64 @@ require_once '../includes/header.php';
|
||||
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: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
||||
'Accept': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: new URLSearchParams({ praticien_id: praticienId }).toString()
|
||||
body: formData
|
||||
})
|
||||
.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;
|
||||
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: deja ? 'info' : 'success',
|
||||
title: <?= json_encode($translations['sw_favori_ajoute_title'] ?? 'Favori ajouté !', JSON_UNESCAPED_UNICODE) ?>,
|
||||
text: text,
|
||||
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(text); }
|
||||
} 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.'); }
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'Oups',
|
||||
text: err.message || 'Erreur.'
|
||||
});
|
||||
} else {
|
||||
console.error(err);
|
||||
alert('Erreur.');
|
||||
}
|
||||
})
|
||||
.finally(() => { el.dataset.loading = '0'; });
|
||||
.finally(() => {
|
||||
el.dataset.loading = '0';
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('click', (e)=>{
|
||||
@@ -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,8 +1,6 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../includes/init_client.php';
|
||||
|
||||
|
||||
|
||||
$client_id = $_SESSION['reference_id'];
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
@@ -16,14 +14,17 @@ $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){
|
||||
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') . '…';
|
||||
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
|
||||
@@ -35,33 +36,33 @@ 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 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>
|
||||
@@ -71,43 +72,75 @@ require_once '../includes/header.php';
|
||||
</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) {
|
||||
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: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
body: 'praticien_id=' + praticienId
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: formData
|
||||
})
|
||||
.then(res => res.text())
|
||||
.then(msg => {
|
||||
.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: <?= json_encode($translations['favori_supprime_title']) ?>,
|
||||
text: msg,
|
||||
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();
|
||||
});
|
||||
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,8 +1,6 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../includes/init_client.php';
|
||||
|
||||
|
||||
|
||||
$pdo->prepare("
|
||||
INSERT INTO push_badge_counters (compte_id, msg_unread, rdv_unread, updated_at)
|
||||
VALUES (:cid, 0, 0, NOW())
|
||||
@@ -11,8 +9,6 @@ $pdo->prepare("
|
||||
updated_at = NOW()
|
||||
")->execute([':cid' => (int) $_SESSION['compte_id']]);
|
||||
|
||||
|
||||
|
||||
$client_id = $_SESSION['reference_id'];
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
@@ -41,27 +37,27 @@ 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']);
|
||||
@@ -69,33 +65,33 @@ require_once '../includes/header.php';
|
||||
$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>
|
||||
|
||||
@@ -117,8 +113,8 @@ require_once '../includes/header.php';
|
||||
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']) ?>
|
||||
<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 {
|
||||
@@ -126,9 +122,14 @@ require_once '../includes/header.php';
|
||||
?>
|
||||
<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
|
||||
@@ -137,13 +138,13 @@ require_once '../includes/header.php';
|
||||
// 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 {
|
||||
?>
|
||||
<small class="text-muted d-block mt-1">
|
||||
<?= htmlspecialchars($translations['statut_annule'] ?? 'Annulé') ?>
|
||||
<?php echo htmlspecialchars($translations['statut_annule'] ?? 'Annulé') ?>
|
||||
</small>
|
||||
<?php
|
||||
}
|
||||
@@ -152,7 +153,7 @@ require_once '../includes/header.php';
|
||||
// --- 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'] ?? ''))
|
||||
trim((string) ($r['p_cp'] ?? '')) . ' ' . trim((string) ($r['p_ville'] ?? '')),
|
||||
], fn($x) => $x !== '' && $x !== ' ');
|
||||
$addr = trim(implode(', ', $addrParts));
|
||||
|
||||
@@ -160,8 +161,8 @@ require_once '../includes/header.php';
|
||||
//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') ?>
|
||||
<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
|
||||
}
|
||||
@@ -175,15 +176,15 @@ 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) => {
|
||||
@@ -192,15 +193,18 @@ function annulerRdv(id) {
|
||||
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) })
|
||||
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>
|
||||
|
||||
+27
-25
@@ -14,6 +14,7 @@ if (!$client) {
|
||||
|
||||
// ---- 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'] ?? '');
|
||||
@@ -38,7 +39,7 @@ if (($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
|
||||
'adresse' => $adresse,
|
||||
'ville' => $ville,
|
||||
'code_postal' => $cp,
|
||||
'id' => $id
|
||||
'id' => $id,
|
||||
]);
|
||||
|
||||
// redirection (évite le repost + actualise la page infos)
|
||||
@@ -54,60 +55,61 @@ require_once __DIR__ . '/../includes/header.php';
|
||||
|
||||
|
||||
<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) {
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'message' => 'Méthode non autorisée.',
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("DELETE FROM praticien_client WHERE client_id = ? AND praticien_id = ?");
|
||||
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,8 +292,13 @@
|
||||
<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 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>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// ⚠️ 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);
|
||||
|
||||
// =====================================================
|
||||
@@ -65,7 +64,6 @@ if ($pid && $sec_uid) {
|
||||
$permRow = $q->fetch(PDO::FETCH_ASSOC) ?: null;
|
||||
}
|
||||
|
||||
|
||||
// =====================================================
|
||||
// [RETOUR / REFERER SÉCURISÉ]
|
||||
// =====================================================
|
||||
@@ -74,7 +72,10 @@ $host = $_SERVER['HTTP_HOST'] ?? '';
|
||||
$safeRef = '';
|
||||
if ($ref) {
|
||||
$u = parse_url($ref);
|
||||
if (!empty($u['host']) && $u['host'] === $host) $safeRef = $ref;
|
||||
if (! empty($u['host']) && $u['host'] === $host) {
|
||||
$safeRef = $ref;
|
||||
}
|
||||
|
||||
}
|
||||
$fallbackBack = $safeRef ?: '/public/cabinet/dashboard.php';
|
||||
|
||||
@@ -88,8 +89,8 @@ $is_mobile = preg_match('/Mobile|Android|iPhone|iPad/i', $_SERVER['HTTP_USER_AGE
|
||||
<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 -->
|
||||
|
||||
@@ -98,11 +99,11 @@ $is_mobile = preg_match('/Mobile|Android|iPhone|iPad/i', $_SERVER['HTTP_USER_AGE
|
||||
<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="<?php echo asset('/includes/site_style.css') ?>">
|
||||
<link rel="stylesheet" href="<?php echo 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/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,37 +164,42 @@ $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') ?>
|
||||
<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') ?>
|
||||
<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') ?>
|
||||
<?php echo htmlspecialchars($translations['contact_admin'] ?? 'Contact Admin') ?>
|
||||
</li>
|
||||
<li onclick="location.href='/public/deconnexion.php'">
|
||||
<?= htmlspecialchars($translations['menu_deconnexion'] ?? 'Déconnexion') ?>
|
||||
<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>
|
||||
|
||||
@@ -8,14 +8,16 @@ 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 {
|
||||
function enforce_session_timeout(int $maxIdleSeconds = 7200): void
|
||||
{
|
||||
$now = time();
|
||||
$last = (int) ($_SESSION['last_activity'] ?? 0);
|
||||
|
||||
@@ -26,55 +28,74 @@ 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 {
|
||||
function refresh_role_from_db(PDO $pdo): string
|
||||
{
|
||||
$compteId = (int) ($_SESSION['compte_id'] ?? 0);
|
||||
if ($compteId <= 0) return '';
|
||||
if ($compteId <= 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("SELECT role FROM compte WHERE id=? LIMIT 1");
|
||||
$stmt->execute([$compteId]);
|
||||
@@ -93,7 +114,8 @@ 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 {
|
||||
function require_login_or_redirect(?array $allowedRoles = null, array $options = []): void
|
||||
{
|
||||
if (! is_logged_in()) {
|
||||
redirect_to_login_with_next($_SERVER['REQUEST_URI'] ?? '/');
|
||||
}
|
||||
@@ -134,7 +156,8 @@ 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));
|
||||
}
|
||||
@@ -142,7 +165,8 @@ function ensure_csrf_token(): string {
|
||||
}
|
||||
|
||||
/** 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');
|
||||
@@ -160,9 +184,12 @@ function require_post_csrf(): void {
|
||||
}
|
||||
|
||||
/** 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'] ?? '',
|
||||
]);
|
||||
}
|
||||
+207
-42
@@ -1055,54 +1055,96 @@ Cliquez simplement sur le lien pour vous connecter.<br>',
|
||||
'howitworks_goal_text' => 'vous mettre en relation avec le bon spécialiste russophone en France, vous aider à clarifier votre besoin et trouver une solution. Simple et pratique !',
|
||||
|
||||
// ===== SEO FAQ =====
|
||||
'seo_faq_title' => 'Questions & thèmes — guide des rubriques | USLUGI.FR',
|
||||
'seo_faq_desc' => 'Accès rapide aux rubriques et réponses aux questions fréquentes : fonctionnement, réservation, paiement, documents. Thèmes : immigration, mariage & famille, travail, impôts, santé, création d’entreprise.',
|
||||
'seo_faq_keywords' => 'FAQ, uslugi.fr, comment ça marche, consultation en ligne, immigration, mariage, divorce, PACS, travail, impôts, santé, création entreprise, spécialistes russophones France',
|
||||
|
||||
'faq_meta_title' => 'FAQ — USLUGI.FR | Spécialistes russophones en France',
|
||||
'faq_meta_description' => 'Questions fréquentes sur Uslugi.fr — trouvez un avocat russophone à Paris, dentiste, psychologue, notaire, agent immobilier, conseiller financier ou traducteur assermenté en France. Réservation en ligne.',
|
||||
'faq_meta_keywords' => 'avocat russophone Paris, notaire russophone France, psychologue russe en ligne, dentiste russe Paris, agent immobilier russe France, traducteur assermenté russe France, Uslugi.fr',
|
||||
// ===== FAQ Page =====
|
||||
'faq_help_aria' => 'Aide',
|
||||
'faq_badge' => 'Aide',
|
||||
'faq_title' => 'Questions fréquentes',
|
||||
'faq_intro' => 'Réponses aux questions essentielles sur la plateforme et les consultations.',
|
||||
'faq_intro' => 'Tout sur la recherche de spécialistes russophones en France : avocat, notaire, psychologue, dentiste, agent immobilier, traducteur et autres',
|
||||
|
||||
'faq_block1_title' => 'Questions techniques',
|
||||
'faq_q1' => 'Comment fonctionne le site uslugi.fr ?',
|
||||
'faq_a1' => 'Uslugi.fr est une plateforme de consultations (en ligne ou parfois en présentiel) avec des experts : avocats, notaires, médecins, psychologues, traducteurs et autres spécialistes. Vous pouvez réserver et faire une visioconsultation sur le site via Jitsi.',
|
||||
'faq_q2' => 'Comment prendre rendez-vous sur uslugi.fr ?',
|
||||
'faq_a2' => 'Choisissez un spécialiste, un créneau et convenez du mode de paiement directement avec lui. Après la réservation, vous recevez un email avec la confirmation (et le lien si c’est en ligne).',
|
||||
'faq_q3' => 'Comment envoyer des documents au spécialiste ?',
|
||||
'faq_a3' => 'Dans le chat, vous pouvez joindre des fichiers (scans, contrats, images médicales). Cela aide le spécialiste à préparer la consultation.',
|
||||
'faq_q4' => 'Que faire si le spécialiste ne se connecte pas à l’heure ?',
|
||||
'faq_a4' => 'Attendez quelques minutes et rafraîchissez la page. Si besoin, contactez le support uslugi.fr : nous contacterons le spécialiste et aiderons à résoudre la situation.',
|
||||
'faq_q5' => 'Comment se passe le paiement des consultations ?',
|
||||
'faq_a5' => 'Le paiement se fait directement auprès du spécialiste — uslugi.fr n’encaisse pas les paiements. Les options (virement, PayPal, etc.) sont indiquées dans l’espace personnel. Vous pouvez уточнить детали dans le chat.',
|
||||
'faq_q6' => 'Dans quelle langue se déroule la consultation ?',
|
||||
'faq_a6' => 'Les consultations se déroulent en russe. Si un spécialiste a un nom/prénom français, cela signifie qu’il parle russe ou qu’un assistant russophone assure la consultation.',
|
||||
'faq_tabs_aria' => 'Catégories FAQ',
|
||||
'faq_tab_all' => 'Toutes les questions',
|
||||
'faq_tab_find' => 'Trouver un spécialiste',
|
||||
'faq_tab_client' => 'Clients',
|
||||
'faq_tab_specialist' => 'Spécialistes',
|
||||
|
||||
'faq_block2_title' => 'Thèmes et spécialistes',
|
||||
'faq_q7' => 'Comment les spécialistes sont-ils sélectionnés sur uslugi.fr ?',
|
||||
'faq_a7' => 'Les spécialistes sont vérifiés et doivent disposer d’un numéro SIRET valide, confirmant leur enregistrement officiel en France.',
|
||||
'faq_q8' => 'Quels spécialistes sont présents et sur quels sujets ?',
|
||||
'faq_a8_intro' => 'Sur uslugi.fr, vous trouverez des juristes, notaires, psychologues, traducteurs et médecins. Ils conseillent sur :',
|
||||
'faq_a8_and' => 'et',
|
||||
'faq_q9' => 'Comment se déroulent les consultations médicales sur uslugi.fr ?',
|
||||
'faq_a9' => 'Les médecins réalisent une première consultation en ligne. Vous pouvez joindre des documents et des images dans le chat. Si nécessaire, le médecin vous orientera vers un examen en présentiel.',
|
||||
'faq_q10' => 'Comment se déroulent les consultations avec notaires et traducteurs ?',
|
||||
'faq_a10_intro' => 'Les notaires conseillent sur la famille, les successions, l’immobilier ou la création d’entreprise. Les traducteurs préparent des traductions officielles pour',
|
||||
'faq_a10_or' => 'ou',
|
||||
'faq_q11' => 'Peut-on parler de fiscalité et de gestion financière ?',
|
||||
'faq_a11_intro' => 'Oui, les spécialistes peuvent vous aider à comprendre comptes, déclarations et courriers. Voir la rubrique',
|
||||
'faq_q12' => 'Quels sujets peut-on aborder en consultation ?',
|
||||
'faq_a12_intro' => 'Nos spécialistes vous accompagnent sur les sujets de la vie en France :',
|
||||
'faq_a12_outro' => 'Même un dossier complexe peut être discuté en ligne ou en présentiel pour obtenir des recommandations et un plan d’action.',
|
||||
'faq_q_find_lawyer_paris' => 'Où trouver un avocat russophone à Paris ?',
|
||||
'faq_a_find_lawyer_paris' => 'Sur Uslugi.fr, vous trouverez des avocats russophones à Paris, Nice, Lyon et dans d’autres villes de France. Tous les avocats sont officiellement enregistrés et disposent d’un numéro SIRET. La prise de rendez-vous en ligne se fait en quelques minutes.',
|
||||
|
||||
'faq_topic_immigration' => 'immigration',
|
||||
'faq_topic_family' => 'mariage & famille',
|
||||
'faq_topic_jobs' => 'travail',
|
||||
'faq_topic_taxes' => 'impôts',
|
||||
'faq_topic_health' => 'santé',
|
||||
'faq_topic_business' => 'création d’entreprise',
|
||||
'faq_q_divorce_lawyer' => 'Existe-t-il un avocat russophone spécialisé en divorce en France ?',
|
||||
'faq_a_divorce_lawyer' => 'Oui. Uslugi.fr réunit des avocats russophones en droit de la famille en France : divorce, partage des biens, garde des enfants. Prenez rendez-vous en ligne et obtenez de l’aide en russe.',
|
||||
|
||||
'faq_btn_find' => 'Trouver un spécialiste',
|
||||
'faq_q_find_notary' => 'Où trouver un notaire russophone en France ?',
|
||||
'faq_a_find_notary' => 'Sur Uslugi.fr, vous pouvez trouver un notaire russophone en France pour les successions, l’achat immobilier ou la rédaction d’un contrat de mariage. Prise de rendez-vous en ligne et consultation en russe.',
|
||||
|
||||
'faq_q_find_real_estate' => 'Comment trouver un agent immobilier russophone en France ?',
|
||||
'faq_a_find_real_estate' => 'Uslugi.fr propose des agents immobiliers russophones en France, à Paris, Nice et dans d’autres villes. Ils peuvent vous accompagner pour acheter, vendre ou louer un bien et gérer la transaction en russe.',
|
||||
|
||||
'faq_q_find_dentist_paris' => 'Où trouver un dentiste russophone à Paris ?',
|
||||
'faq_a_find_dentist_paris' => 'Sur Uslugi.fr, vous pouvez prendre rendez-vous avec un dentiste russophone en France. Consultation en russe, sans barrière de langue, avec prise de rendez-vous en ligne en quelques minutes.',
|
||||
|
||||
'faq_q_find_orthodontist' => 'Existe-t-il un orthodontiste russophone en France ?',
|
||||
'faq_a_find_orthodontist' => 'Oui. Uslugi.fr propose des orthodontistes russophones en France. Prise de rendez-vous en ligne et consultation en russe.',
|
||||
|
||||
'faq_q_find_psychologist_online' => 'Existe-t-il un psychologue russophone en ligne en France ?',
|
||||
'faq_a_find_psychologist_online' => 'Oui. Uslugi.fr propose des psychologues russophones en France qui consultent en ligne. Les consultations sont accessibles depuis n’importe où dans le monde, notamment en cas d’émigration, d’adaptation ou de crise personnelle.',
|
||||
|
||||
'faq_q_find_financial_advisor' => 'Où trouver un conseiller financier russophone en France ?',
|
||||
'faq_a_find_financial_advisor' => 'Sur Uslugi.fr, vous trouverez des conseillers financiers russophones en France pour les investissements, assurances, produits bancaires et la gestion de patrimoine. Consultation en russe et prise de rendez-vous en ligne.',
|
||||
|
||||
'faq_q_residence_permit' => 'Peut-on obtenir une consultation sur le titre de séjour en France en russe ?',
|
||||
'faq_a_residence_permit' => 'Oui. Sur Uslugi.fr, vous trouverez des avocats et consultants russophones spécialisés dans les questions de titre de séjour en France. Ils peuvent vous aider à comprendre les documents, procédures et droits en russe.',
|
||||
|
||||
'faq_q_find_repair_master' => 'Où trouver un artisan russophone en France ?',
|
||||
'faq_a_find_repair_master' => 'Uslugi.fr propose des artisans russophones en France. Vous pouvez décrire votre besoin et demander une consultation ou l’intervention d’un spécialiste.',
|
||||
|
||||
'faq_q_funeral_services' => 'Existe-t-il une entreprise funéraire russophone en France ?',
|
||||
'faq_a_funeral_services' => 'Oui. Sur Uslugi.fr, vous pouvez trouver des professionnels russophones des services funéraires en France. Dans un moment difficile, il est important de pouvoir être accompagné dans sa langue maternelle.',
|
||||
|
||||
'faq_q_sworn_translator' => 'Où trouver un traducteur assermenté du russe en France ?',
|
||||
'faq_a_sworn_translator' => 'Sur Uslugi.fr, vous trouverez des traducteurs assermentés du russe en France pour les documents officiels, audiences, actes notariés et autres démarches juridiques. Prise de rendez-vous en ligne.',
|
||||
|
||||
'faq_q_what_is_uslugi' => 'Qu’est-ce que Uslugi.fr ?',
|
||||
'faq_a_what_is_uslugi' => 'Uslugi.fr est une plateforme pour trouver des spécialistes russophones en France : avocats, notaires, agents immobiliers, psychologues, dentistes, traducteurs et autres. Trouvez un spécialiste, réservez en ligne et obtenez une consultation en visioconférence, le tout au même endroit.',
|
||||
|
||||
'faq_q_book_consultation' => 'Comment prendre rendez-vous pour une consultation ?',
|
||||
'faq_a_book_consultation' => 'Créez un compte personnel, choisissez le spécialiste souhaité dans l’annuaire, puis sélectionnez une date et une heure disponibles dans son agenda. Après la réservation, vous recevrez un email de confirmation.',
|
||||
|
||||
'faq_q_consultation_price' => 'Combien coûte une consultation ?',
|
||||
'faq_a_consultation_price' => 'Uslugi.fr ne prend aucune commission. Chaque spécialiste fixe librement ses tarifs et le paiement se fait directement entre le client et le spécialiste. Le prix est indiqué sur le profil du spécialiste.',
|
||||
|
||||
'faq_q_online_consultation' => 'Comment se déroule une consultation en ligne ?',
|
||||
'faq_a_online_consultation' => 'La consultation se déroule en visioconférence via Jitsi, directement sur le site Uslugi.fr. Il n’est pas nécessaire d’installer une application supplémentaire : il suffit de suivre le lien reçu dans l’email de confirmation.',
|
||||
|
||||
'faq_q_contact_before_consultation' => 'Peut-on contacter le spécialiste avant la consultation ?',
|
||||
'faq_a_contact_before_consultation' => 'Oui. Après la réservation, vous pouvez écrire au spécialiste dans le chat de la plateforme pour préciser les détails, poser des questions ou transmettre des documents.',
|
||||
|
||||
'faq_q_cancel_reschedule' => 'Comment annuler ou reporter un rendez-vous ?',
|
||||
'faq_a_cancel_reschedule' => 'Les conditions d’annulation sont fixées individuellement par chaque spécialiste. Consultez la politique d’annulation sur son profil ou échangez directement avec lui dans le chat.',
|
||||
|
||||
'faq_q_verified_specialists' => 'Les spécialistes sont-ils vérifiés ?',
|
||||
'faq_a_verified_specialists' => 'Tous les spécialistes de la plateforme indiquent leur numéro SIRET, identifiant officiel en France pour les professionnels et entreprises. Cela confirme qu’ils sont officiellement enregistrés en France.',
|
||||
|
||||
'faq_q_cities' => 'Dans quelles villes de France les spécialistes exercent-ils ?',
|
||||
'faq_a_cities' => 'La plateforme rassemble des spécialistes de nombreuses villes de France : Paris, Nice, Lyon, Marseille, Bordeaux, Strasbourg et d’autres. La majorité des consultations sont aussi disponibles en ligne, depuis n’importe où en France ou dans le monde.',
|
||||
|
||||
'faq_q_register_specialist' => 'Je suis spécialiste. Comment m’inscrire sur la plateforme ?',
|
||||
'faq_a_register_specialist' => 'Rendez-vous sur la page d’inscription des spécialistes sur Uslugi.fr, complétez votre profil, indiquez votre numéro SIRET et configurez votre agenda. L’inscription prend quelques minutes. Ensuite, les clients pourront vous trouver dans l’annuaire et prendre rendez-vous en ligne.',
|
||||
|
||||
'faq_q_specialist_price' => 'Combien coûte l’inscription pour un spécialiste ?',
|
||||
'faq_a_specialist_price' => 'Uslugi.fr ne prend pas de commission sur les consultations. Les conditions de présence sur la plateforme sont indiquées lors de l’inscription.',
|
||||
|
||||
'faq_q_manage_schedule' => 'Comment gérer son agenda ?',
|
||||
'faq_a_manage_schedule' => 'Dans l’espace spécialiste, un agenda flexible permet d’indiquer soi-même les jours et horaires disponibles. Les clients ne voient que les créneaux libres et ne peuvent pas réserver sur un horaire occupé.',
|
||||
|
||||
'faq_q_client_payment' => 'Comment se passe le paiement des clients ?',
|
||||
'faq_a_client_payment' => 'Le paiement s’effectue directement entre le client et le spécialiste. Uslugi.fr n’intervient pas dans les transactions financières et ne prend aucune commission sur les consultations.',
|
||||
|
||||
'faq_cta_title' => 'Vous n’avez pas trouvé la réponse à votre question ?',
|
||||
'faq_cta_text' => 'Écrivez-nous, nous vous répondrons dans les plus brefs délais.',
|
||||
'faq_cta_contact' => 'Nous écrire',
|
||||
'faq_cta_specialist' => 'Devenir spécialiste',
|
||||
|
||||
// ===== SEO (topic business) =====
|
||||
'seo_topic_business_title' => 'Créer une entreprise en France — statuts, impôts, obligations | USLUGI.FR',
|
||||
@@ -2073,4 +2115,127 @@ Cliquez simplement sur le lien pour vous connecter.<br>',
|
||||
'mailing_clients_err_csrf' => 'Erreur de sécurité. Rechargez la page.',
|
||||
'mailing_clients_err_no_emails' => 'Les clients sélectionnés n’ont pas d’adresse email.',
|
||||
'mail_clients' => 'Mailing clients',
|
||||
|
||||
'boostseo_home_h1_suffix' => ' — spécialistes russophones en France : avocat, psychologue, médecin, traducteur',
|
||||
'boostseo_home_intro' => 'Plateforme pour trouver un spécialiste russophone en France : avocat à Nice, psychologue à Paris, médecin à Lyon. Réservation en ligne rapide sur Uslugi.fr.',
|
||||
'boostseo_home_h2_categories' => 'Spécialistes russophones par catégorie en France',
|
||||
'boostseo_home_h2_recent' => 'Spécialistes récemment disponibles',
|
||||
'boostseo_home_h2_cities' => 'Spécialistes russophones par ville',
|
||||
'boostseo_home_cities_text' => 'Paris, Nice, Lyon, Marseille, Bordeaux, Strasbourg — trouvez un spécialiste russophone dans votre ville : avocat, psychologue, médecin, traducteur.',
|
||||
'boostseo_search_title_default' => 'Spécialistes russophones en France | Recherche — Uslugi.fr',
|
||||
'boostseo_search_title_spec' => '%specialite% en France | Rendez-vous en ligne — Uslugi.fr',
|
||||
'boostseo_search_title_spec_city' => '%specialite% à %ville% | Rendez-vous en ligne — Uslugi.fr',
|
||||
'boostseo_search_title_name' => '%specialite% | Recherche de spécialiste — Uslugi.fr',
|
||||
|
||||
'boostseo_search_desc_default' => 'Trouvez un spécialiste russophone en France : avocat, psychologue, médecin, traducteur. Réservation en ligne sur Uslugi.fr.',
|
||||
'boostseo_search_desc_spec' => 'Trouvez un spécialiste russophone pour la recherche %specialite% en France. Réservation en ligne, chat et visioconsultation sur Uslugi.fr.',
|
||||
'boostseo_search_desc_spec_city' => 'Trouvez un spécialiste russophone pour la recherche %specialite% à %ville%. Réservation en ligne, chat et visioconsultation sur Uslugi.fr.',
|
||||
|
||||
'boostseo_search_intro_default' => 'Cette page regroupe des spécialistes russophones en France disponibles à la réservation en ligne.',
|
||||
'boostseo_search_intro_spec' => 'Résultats de recherche pour %specialite% parmi les spécialistes russophones en France.',
|
||||
'boostseo_search_intro_spec_city' => 'Résultats de recherche pour %specialite% à %ville% parmi les spécialistes russophones en France.',
|
||||
|
||||
'form_index_badge' => 'Gratuit · Sans inscription',
|
||||
'form_index_title' => 'Vous ne trouvez pas le bon spécialiste ?',
|
||||
'form_index_intro' => 'Décrivez votre situation : nous sélectionnerons nous-mêmes un spécialiste russophone adapté en France et nous vous recontacterons.',
|
||||
|
||||
'form_index_name_label' => 'Votre nom',
|
||||
'form_index_name_placeholder' => 'Comment vous appelez-vous ?',
|
||||
|
||||
'form_index_category_label' => 'Spécialiste recherché',
|
||||
'form_index_category_placeholder' => 'Choisissez une catégorie',
|
||||
'form_index_category_lawyer' => 'Avocat',
|
||||
'form_index_category_notary' => 'Notaire',
|
||||
'form_index_category_real_estate' => 'Agent immobilier',
|
||||
'form_index_category_psychologist' => 'Psychologue',
|
||||
'form_index_category_dentist_ortho' => 'Dentiste / Orthodontiste',
|
||||
'form_index_category_financial_advisor' => 'Conseiller financier',
|
||||
'form_index_category_translator' => 'Traducteur / Traducteur assermenté',
|
||||
'form_index_category_residence_consultant' => 'Consultant titre de séjour',
|
||||
'form_index_category_repair' => 'Artisan / Réparation',
|
||||
'form_index_category_funeral' => 'Pompes funèbres',
|
||||
'form_index_category_other' => 'Autre',
|
||||
|
||||
'form_index_situation_label' => 'Décrivez votre situation',
|
||||
'form_index_situation_placeholder' => 'Expliquez brièvement ce qui s’est passé, ce qu’il faut faire, dans quelle ville vous vous trouvez…',
|
||||
|
||||
'form_index_contact_label' => 'Comment vous contacter',
|
||||
'form_index_contact_tab_email' => 'Email',
|
||||
'form_index_contact_tab_wa' => 'WhatsApp',
|
||||
'form_index_email_placeholder' => 'votre@email.com',
|
||||
'form_index_wa_placeholder' => '+33 6 00 00 00 00',
|
||||
|
||||
'form_index_submit' => 'Trouver un spécialiste',
|
||||
'form_index_note' => 'Nous ne transmettons pas vos données à des tiers et nous vous recontacterons dans les prochaines heures.',
|
||||
|
||||
'form_index_success_title' => 'Demande reçue !',
|
||||
'form_index_success_text' => 'Nous allons étudier votre situation et vous recontacter rapidement pour vous proposer un spécialiste adapté.',
|
||||
|
||||
'form_index_error_required' => 'Merci de remplir tous les champs obligatoires.',
|
||||
'form_index_error_send' => 'Une erreur est survenue lors de l’envoi. Merci de réessayer.',
|
||||
'form_index_error_popup_title' => 'Erreur',
|
||||
|
||||
'regist_meta_title' => 'Inscription spécialiste sur USLUGI.FR',
|
||||
'regist_meta_description' => 'Créez votre profil professionnel sur USLUGI.FR et recevez de nouveaux clients en France.',
|
||||
'regist_meta_keywords' => 'spécialiste, inscription, profil, clients, France, consultation, uslugi',
|
||||
|
||||
'regist_tag' => 'Pour les spécialistes',
|
||||
'regist_h1' => "Vous êtes spécialiste ?\nCréez votre profil sur USLUGI.FR",
|
||||
'regist_hero_text' => 'Recevez de nouveaux clients en France !',
|
||||
'regist_btn_create_profile' => 'Créer un profil',
|
||||
|
||||
'regist_benefits_title' => 'Ce que vous obtenez',
|
||||
|
||||
'regist_benefit_1_title' => 'Un profil avec vos informations',
|
||||
'regist_benefit_1_text' => 'Spécialité, ville, photo, description.',
|
||||
|
||||
'regist_benefit_2_title' => 'Un calendrier de réservation',
|
||||
'regist_benefit_2_text' => 'Le client voit vos disponibilités et prend rendez-vous lui-même.',
|
||||
|
||||
'regist_benefit_3_title' => 'La visioconférence sur la plateforme',
|
||||
'regist_benefit_3_text' => 'Des consultations en ligne sans Zoom ni réglages compliqués.',
|
||||
|
||||
'regist_benefit_4_title' => 'Un chat avec le client',
|
||||
'regist_benefit_4_text' => 'Échangez des documents et précisez les détails avant le rendez-vous.',
|
||||
|
||||
'regist_benefit_5_title' => 'Une audience ciblée',
|
||||
'regist_benefit_5_text' => 'Les utilisateurs viennent avec une demande précise. Une partie réserve seule, une autre est orientée directement vers vous via notre formulaire de mise en relation.',
|
||||
|
||||
'regist_pricing_title' => 'Tarifs',
|
||||
'regist_pricing_sub' => 'Sans commission sur vos consultations — paiement direct par le client',
|
||||
'regist_pricing_badge' => 'Le plus avantageux',
|
||||
|
||||
'regist_yearly_label' => 'Par an',
|
||||
'regist_yearly_price' => '275€',
|
||||
'regist_yearly_unit' => '23€ par mois',
|
||||
|
||||
'regist_monthly_label' => 'Par mois',
|
||||
'regist_monthly_price' => '25€',
|
||||
'regist_monthly_unit' => '',
|
||||
|
||||
'regist_btn_choose' => 'Choisir',
|
||||
'regist_pricing_note' => 'Paiement par carte directement sur le site via Stripe',
|
||||
|
||||
'regist_steps_title' => 'Comment commencer',
|
||||
|
||||
'regist_step_1_title' => 'Choisissez votre abonnement et payez par carte',
|
||||
'regist_step_1_text' => 'Directement sur le site, rapidement et en toute sécurité.',
|
||||
|
||||
'regist_step_2_title' => 'Créez votre profil',
|
||||
'regist_step_2_text' => 'Photo, description, spécialité, calendrier — vous gérez tout vous-même.',
|
||||
|
||||
'regist_step_3_title' => 'Recevez des demandes de clients',
|
||||
'regist_step_3_text' => 'Notifications par email, réservation au calendrier, visioconférence.',
|
||||
|
||||
'regist_help_title' => 'Pas le temps de remplir votre profil ?',
|
||||
'regist_help_text' => 'Après le paiement, envoyez-nous votre photo et vos informations — nous le faisons pour vous.',
|
||||
|
||||
'regist_cta_title' => 'Prêt à commencer ?',
|
||||
'regist_cta_text' => 'L’inscription prend 5 minutes',
|
||||
'label_mdp_actuel' => 'Mot de passe actuel',
|
||||
'placeholder_mdp_actuel' => 'Entrez votre mot de passe actuel',
|
||||
'erreur_mdp_actuel_incorrect' => 'Mot de passe actuel incorrect.',
|
||||
'trop_de_tentatives_connexion' => 'Trop de tentatives de connexion. Merci de réessayer un peu plus tard.',
|
||||
'verif_code_trop_tentatives' => 'Trop de tentatives. Veuillez recommencer la procédure de réinitialisation.',
|
||||
'mdp_oublie_message_neutre' => 'Si un compte existe pour cette adresse, un code a été envoyé.',
|
||||
];
|
||||
File diff suppressed because it is too large.
Load diff
+208
-42
@@ -1064,54 +1064,97 @@ return [
|
||||
'howitworks_goal_text' => 'соединить вас с нужным русскоязычным специалистом во Франции, помочь разобраться в вопросе и найти решение. Всё просто и удобно!',
|
||||
|
||||
// ===== SEO FAQ =====
|
||||
'seo_faq_title' => 'Вопросы и темы — гид по разделам | USLUGI.FR',
|
||||
'seo_faq_desc' => 'Быстрый доступ к разделам и ответы на частые вопросы: как работает платформа, запись, оплата, документы. Темы: иммиграция, брак и семья, работа, налоги, медицина, открытие бизнеса.',
|
||||
'seo_faq_keywords' => 'FAQ, uslugi.fr, как работает сайт, консультации онлайн, иммиграция, брак, развод, ПАКС, работа, налоги, медицина, открытие бизнеса, русскоязычные специалисты Франция',
|
||||
'faq_meta_title' => 'Вопросы и ответы — USLUGI.FR | Русскоязычные специалисты во Франции',
|
||||
'faq_meta_description' => 'Часто задаваемые вопросы о платформе Uslugi.fr — русский адвокат в Париже, стоматолог, психолог, нотариус, риелтор, финансовый консультант, присяжный переводчик во Франции. Онлайн-запись.',
|
||||
'faq_meta_keywords' => 'русский адвокат Париж, русскоязычный нотариус Франция, русский психолог онлайн, русский стоматолог Париж, русский риелтор Франция, присяжный переводчик русский Франция, Uslugi.fr',
|
||||
|
||||
// ===== FAQ Page =====
|
||||
'faq_help_aria' => 'Помощь',
|
||||
'faq_title' => 'Частые вопросы',
|
||||
'faq_intro' => 'Ответы на основные вопросы о платформе и консультациях.',
|
||||
'faq_badge' => 'Помощь',
|
||||
'faq_title' => 'Вопросы и ответы',
|
||||
'faq_intro' => 'Всё о поиске русскоязычных специалистов во Франции — адвоката, нотариуса, психолога, стоматолога, риелтора, переводчика и других',
|
||||
|
||||
'faq_block1_title' => 'Технические вопросы',
|
||||
'faq_q1' => 'Как работает сайт uslugi.fr?',
|
||||
'faq_a1' => 'Uslugi.fr — это платформа онлайн-консультаций с экспертами: адвокатами, нотариусами, врачами, психологами, переводчиками и другими специалистами. Вы можете записаться и получить видеоконсультацию прямо на сайте через Jitsi. Также возможна офлайн-консультация, если такая опция есть в календаре специалиста.',
|
||||
'faq_q2' => 'Как записаться на консультацию на uslugi.fr?',
|
||||
'faq_a2' => 'Выберите специалиста, удобное время и согласуйте способ оплаты напрямую с ним. После записи вы получите ссылку на онлайн-встречу или подтверждение офлайн-встречи.',
|
||||
'faq_q3' => 'Как отправлять документы специалисту?',
|
||||
'faq_a3' => 'В чате можно прикреплять файлы: сканы документов, договоры или медицинские снимки. Это помогает специалисту подготовиться к консультации заранее.',
|
||||
'faq_q4' => 'Что делать, если специалист не подключился вовремя?',
|
||||
'faq_a4' => 'Подождите несколько минут и обновите страницу. Если специалист не появится, напишите в поддержку uslugi.fr — мы свяжемся со специалистом и поможем решить вопрос.',
|
||||
'faq_q5' => 'Как происходит оплата консультаций?',
|
||||
'faq_a5' => 'Оплата производится напрямую специалисту — платформа uslugi.fr не принимает платежи. В личном кабинете указаны варианты оплаты (перевод, PayPal и т.д.). Детали можно уточнить в чате.',
|
||||
'faq_q6' => 'На каком языке проходит консультация?',
|
||||
'faq_a6' => 'Консультации проходят на русском языке. Если у специалиста французские имя и фамилия, это означает, что он говорит по-русски или консультацию ведёт русскоязычный ассистент.',
|
||||
'faq_tabs_aria' => 'Категории FAQ',
|
||||
'faq_tab_all' => 'Все вопросы',
|
||||
'faq_tab_find' => 'Найти специалиста',
|
||||
'faq_tab_client' => 'Клиентам',
|
||||
'faq_tab_specialist' => 'Специалистам',
|
||||
|
||||
'faq_block2_title' => 'Темы и специалисты',
|
||||
'faq_q7' => 'Каким образом специалисты проходят отбор на uslugi.fr?',
|
||||
'faq_a7' => 'Специалисты проходят проверку и должны иметь действующий номер SIRET, подтверждающий официальную регистрацию во Франции.',
|
||||
'faq_q8' => 'Какие специалисты работают на uslugi.fr и какие темы они ведут?',
|
||||
'faq_a8_intro' => 'На uslugi.fr представлены юристы, нотариусы, психологи, переводчики и врачи. Они консультируют по темам:',
|
||||
'faq_a8_and' => 'и',
|
||||
'faq_q9' => 'Как проходят консультации у врачей на uslugi.fr?',
|
||||
'faq_a9' => 'Врачи проводят первичные онлайн-консультации. В чате можно прикреплять снимки и документы. При необходимости врач направит вас на очное обследование.',
|
||||
'faq_q10' => 'Как проходят консультации у нотариусов и переводчиков на uslugi.fr?',
|
||||
'faq_a10_intro' => 'Нотариусы консультируют по вопросам семьи, наследства, недвижимости или бизнеса. Переводчики готовят официальные переводы документов для',
|
||||
'faq_a10_or' => 'или',
|
||||
'faq_q11' => 'Можно ли обсудить финансовую грамотность и налоги?',
|
||||
'faq_a11_intro' => 'Да, специалисты помогут разобраться со счетами, декларациями и письмами от налоговой. См. раздел',
|
||||
'faq_q12' => 'Какие темы можно обсудить на консультации?',
|
||||
'faq_a12_intro' => 'Наши специалисты помогают по вопросам жизни во Франции:',
|
||||
'faq_a12_outro' => 'Даже сложный случай можно обсудить онлайн или офлайн, чтобы получить рекомендации и план действий.',
|
||||
'faq_q_find_lawyer_paris' => 'Где найти русского адвоката в Париже?',
|
||||
'faq_a_find_lawyer_paris' => 'На Uslugi.fr вы найдёте русскоязычных адвокатов в Париже, Ницце, Лионе и других городах Франции. Все адвокаты официально зарегистрированы и имеют номер SIRET. Запись онлайн — за пару минут.',
|
||||
|
||||
'faq_topic_immigration' => 'иммиграции',
|
||||
'faq_topic_family' => 'брака и семьи',
|
||||
'faq_topic_jobs' => 'работы',
|
||||
'faq_topic_taxes' => 'налогов',
|
||||
'faq_topic_health' => 'медицины',
|
||||
'faq_topic_business' => 'открытия бизнеса',
|
||||
'faq_q_divorce_lawyer' => 'Есть ли адвокат по разводу на русском языке во Франции?',
|
||||
'faq_a_divorce_lawyer' => 'Да. На Uslugi.fr представлены русскоязычные адвокаты по семейному праву во Франции — развод, раздел имущества, вопросы опеки над детьми. Запишитесь на консультацию онлайн и получите помощь на русском языке.',
|
||||
|
||||
'faq_btn_find' => 'Найти специалиста',
|
||||
'faq_q_find_notary' => 'Где найти русскоязычного нотариуса во Франции?',
|
||||
'faq_a_find_notary' => 'На Uslugi.fr можно найти русскоязычного нотариуса во Франции — для оформления наследства, покупки недвижимости, составления брачного контракта. Запись онлайн, консультация на русском языке.',
|
||||
|
||||
'faq_q_find_real_estate' => 'Как найти русского риелтора во Франции?',
|
||||
'faq_a_find_real_estate' => 'На Uslugi.fr представлены русскоязычные риелторы во Франции — в Париже, Ницце и других городах. Они помогут купить, продать или арендовать недвижимость и проведут всю сделку на русском языке.',
|
||||
|
||||
'faq_q_find_dentist_paris' => 'Где найти русского стоматолога в Париже?',
|
||||
'faq_a_find_dentist_paris' => 'На Uslugi.fr можно записаться к русскоязычному стоматологу во Франции. Консультация на русском языке — без языкового барьера, онлайн-запись за пару минут.',
|
||||
|
||||
'faq_q_find_orthodontist' => 'Есть ли русскоязычный ортодонт во Франции?',
|
||||
'faq_a_find_orthodontist' => 'Да. На Uslugi.fr представлены русскоязычные ортодонты во Франции. Запись онлайн, консультация на русском языке.',
|
||||
|
||||
'faq_q_find_psychologist_online' => 'Есть ли русскоязычный психолог онлайн во Франции?',
|
||||
'faq_a_find_psychologist_online' => 'Да. На Uslugi.fr есть русскоязычные психологи во Франции, которые принимают онлайн. Консультации доступны из любой точки мира — особенно актуально при эмиграции, адаптации или личном кризисе.',
|
||||
|
||||
'faq_q_find_financial_advisor' => 'Где найти русского финансового консультанта во Франции?',
|
||||
'faq_a_find_financial_advisor' => 'На Uslugi.fr есть русскоязычные финансовые консультанты во Франции — по вопросам инвестиций, страховок, банковских продуктов и управления капиталом. Консультация на русском языке, запись онлайн.',
|
||||
|
||||
'faq_q_residence_permit' => 'Можно ли получить консультацию по ВНЖ во Франции на русском?',
|
||||
'faq_a_residence_permit' => 'Да. На Uslugi.fr вы найдёте русскоязычных адвокатов и консультантов по вопросам вида на жительство во Франции. Они помогут разобраться в документах, процедурах и правах — на русском языке.',
|
||||
|
||||
'faq_q_find_repair_master' => 'Где найти русскоязычного мастера по ремонту во Франции?',
|
||||
'faq_a_find_repair_master' => 'На Uslugi.fr представлены русскоязычные мастера по ремонту во Франции. Вы можете описать свою задачу и записаться на консультацию или вызов специалиста.',
|
||||
|
||||
'faq_q_funeral_services' => 'Есть ли русскоязычное похоронное бюро во Франции?',
|
||||
'faq_a_funeral_services' => 'Да. На Uslugi.fr можно найти русскоязычных специалистов похоронных служб во Франции. В трудный момент важно получить помощь на родном языке — мы понимаем это.',
|
||||
|
||||
'faq_q_sworn_translator' => 'Где найти присяжного переводчика с русского языка во Франции?',
|
||||
'faq_a_sworn_translator' => 'На Uslugi.fr представлены присяжные переводчики с русского языка во Франции — для официальных документов, судебных заседаний, нотариальных актов и других юридических нужд. Запись онлайн.',
|
||||
|
||||
'faq_q_what_is_uslugi' => 'Что такое Uslugi.fr?',
|
||||
'faq_a_what_is_uslugi' => 'Uslugi.fr — платформа для поиска русскоязычных специалистов во Франции: адвокатов, нотариусов, риелторов, психологов, стоматологов, переводчиков и других. Найдите специалиста, запишитесь онлайн и получите консультацию через видеосвязь — всё в одном месте.',
|
||||
|
||||
'faq_q_book_consultation' => 'Как записаться на консультацию?',
|
||||
'faq_a_book_consultation' => 'Создайте личный кабинет, выберите нужного специалиста в каталоге, выберите удобную дату и время в его календаре. После бронирования вы получите письмо с подтверждением на email.',
|
||||
|
||||
'faq_q_consultation_price' => 'Сколько стоит консультация?',
|
||||
'faq_a_consultation_price' => 'Uslugi.fr не берёт комиссию. Каждый специалист устанавливает свои тарифы самостоятельно, оплата происходит напрямую между клиентом и специалистом. Стоимость указана в профиле специалиста.',
|
||||
|
||||
'faq_q_online_consultation' => 'Как проходит онлайн-консультация?',
|
||||
'faq_a_online_consultation' => 'Консультация проходит через видеосвязь Jitsi прямо на сайте Uslugi.fr. Никакого дополнительного приложения устанавливать не нужно — просто перейдите по ссылке из письма-подтверждения.',
|
||||
|
||||
'faq_q_contact_before_consultation' => 'Можно ли связаться со специалистом до консультации?',
|
||||
'faq_a_contact_before_consultation' => 'Да. После записи вы можете написать специалисту в чате на платформе, чтобы уточнить детали, задать вопросы или передать документы.',
|
||||
|
||||
'faq_q_cancel_reschedule' => 'Как отменить или перенести запись?',
|
||||
'faq_a_cancel_reschedule' => 'Условия отмены устанавливает каждый специалист индивидуально. Уточняйте политику отмены в профиле специалиста или в чате с ним.',
|
||||
|
||||
'faq_q_verified_specialists' => 'Специалисты проверены?',
|
||||
'faq_a_verified_specialists' => 'Все специалисты на платформе указывают свой номер SIRET — официальный французский идентификатор юридического лица или предпринимателя. Это подтверждает, что специалист официально зарегистрирован во Франции.',
|
||||
|
||||
'faq_q_cities' => 'В каких городах Франции работают специалисты?',
|
||||
'faq_a_cities' => 'На платформе представлены специалисты из разных городов Франции: Париж, Ницца, Лион, Марсель, Бордо, Страсбург и другие. Большинство консультаций доступны онлайн — из любой точки Франции и мира.',
|
||||
|
||||
'faq_q_register_specialist' => 'Я специалист. Как зарегистрироваться на платформе?',
|
||||
'faq_a_register_specialist' => 'Перейдите на страницу регистрации для специалистов на Uslugi.fr, заполните профиль, укажите номер SIRET и настройте свой календарь. Регистрация занимает несколько минут. После этого клиенты смогут найти вас в каталоге и записаться онлайн.',
|
||||
|
||||
'faq_q_specialist_price' => 'Сколько стоит размещение специалиста?',
|
||||
'faq_a_specialist_price' => 'Uslugi.fr не берёт комиссию с консультаций. Условия размещения указаны при регистрации специалиста на платформе.',
|
||||
|
||||
'faq_q_manage_schedule' => 'Как управлять своим расписанием?',
|
||||
'faq_a_manage_schedule' => 'В личном кабинете специалиста есть гибкий календарь — вы сами указываете доступные дни и часы. Клиенты видят только свободные слоты и не могут записаться на занятое время.',
|
||||
|
||||
'faq_q_client_payment' => 'Как проходит оплата от клиентов?',
|
||||
'faq_a_client_payment' => 'Оплата происходит напрямую между клиентом и специалистом. Uslugi.fr не участвует в финансовых расчётах и не берёт комиссию с консультаций.',
|
||||
|
||||
'faq_cta_title' => 'Не нашли ответ на свой вопрос?',
|
||||
'faq_cta_text' => 'Напишите нам — мы ответим в ближайшее время.',
|
||||
'faq_cta_contact' => 'Написать нам',
|
||||
'faq_cta_specialist' => 'Стать специалистом',
|
||||
|
||||
// ===== SEO (topic business) =====
|
||||
'seo_topic_business_title' => 'Открытие бизнеса во Франции — формы, налоги, отчётность | USLUGI.FR',
|
||||
@@ -2083,4 +2126,127 @@ return [
|
||||
'mailing_clients_err_csrf' => 'Ошибка безопасности. Обновите страницу.',
|
||||
'mailing_clients_err_no_emails' => 'У выбранных клиентов нет email.',
|
||||
'mail_clients' => 'Маилинг клиентам',
|
||||
|
||||
'boostseo_home_h1_suffix' => ' — русскоязычные специалисты во Франции: адвокат, психолог, врач, переводчик',
|
||||
'boostseo_home_intro' => 'Платформа для поиска русскоязычного специалиста во Франции: адвокат в Ницце, психолог в Париже, врач в Лионе. Быстрая онлайн-запись на Uslugi.fr.',
|
||||
'boostseo_home_h2_categories' => 'Русскоязычные специалисты по категориям во Франции',
|
||||
'boostseo_home_h2_recent' => 'Недавно доступные специалисты',
|
||||
'boostseo_home_h2_cities' => 'Русскоязычные специалисты по городам',
|
||||
'boostseo_home_cities_text' => 'Париж, Ницца, Лион, Марсель, Бордо, Страсбург — найдите русскоязычного специалиста в вашем городе: адвоката, психолога, врача, переводчика.',
|
||||
'boostseo_search_title_default' => 'Русскоязычные специалисты во Франции | Поиск специалистов — Uslugi.fr',
|
||||
'boostseo_search_title_spec' => '%specialite% во Франции | Запись онлайн — Uslugi.fr',
|
||||
'boostseo_search_title_spec_city' => '%specialite% в %ville% | Запись онлайн — Uslugi.fr',
|
||||
'boostseo_search_title_name' => '%specialite% | Поиск специалиста — Uslugi.fr',
|
||||
|
||||
'boostseo_search_desc_default' => 'Найдите русскоязычного специалиста во Франции: адвоката, психолога, врача, переводчика. Онлайн-запись на Uslugi.fr.',
|
||||
'boostseo_search_desc_spec' => 'Найдите русскоязычного специалиста по запросу %specialite% во Франции. Онлайн-запись, чат, видеоконсультация на Uslugi.fr.',
|
||||
'boostseo_search_desc_spec_city' => 'Найдите русскоязычного специалиста по запросу %specialite% в %ville%. Онлайн-запись, чат, видеоконсультация на Uslugi.fr.',
|
||||
|
||||
'boostseo_search_intro_default' => 'На этой странице собраны русскоязычные специалисты во Франции с онлайн-записью.',
|
||||
'boostseo_search_intro_spec' => 'Результаты поиска по запросу %specialite% среди русскоязычных специалистов во Франции.',
|
||||
'boostseo_search_intro_spec_city' => 'Результаты поиска по запросу %specialite% в %ville% среди русскоязычных специалистов во Франции.',
|
||||
|
||||
'form_index_badge' => 'Бесплатно · Без регистрации',
|
||||
'form_index_title' => 'Не можете найти нужного специалиста?',
|
||||
'form_index_intro' => 'Опишите вашу ситуацию — мы сами подберём подходящего русскоязычного специалиста во Франции и свяжемся с вами.',
|
||||
|
||||
'form_index_name_label' => 'Ваше имя',
|
||||
'form_index_name_placeholder' => 'Как вас зовут?',
|
||||
|
||||
'form_index_category_label' => 'Нужен специалист',
|
||||
'form_index_category_placeholder' => 'Выберите категорию',
|
||||
'form_index_category_lawyer' => 'Адвокат',
|
||||
'form_index_category_notary' => 'Нотариус',
|
||||
'form_index_category_real_estate' => 'Риелтор',
|
||||
'form_index_category_psychologist' => 'Психолог',
|
||||
'form_index_category_dentist_ortho' => 'Стоматолог / Ортодонт',
|
||||
'form_index_category_financial_advisor' => 'Финансовый консультант',
|
||||
'form_index_category_translator' => 'Переводчик / Присяжный переводчик',
|
||||
'form_index_category_residence_consultant' => 'Консультант по ВНЖ',
|
||||
'form_index_category_repair' => 'Мастер по ремонту',
|
||||
'form_index_category_funeral' => 'Похоронное бюро',
|
||||
'form_index_category_other' => 'Другое',
|
||||
|
||||
'form_index_situation_label' => 'Опишите вашу ситуацию',
|
||||
'form_index_situation_placeholder' => 'Расскажите кратко — что случилось, что нужно сделать, в каком городе вы находитесь…',
|
||||
|
||||
'form_index_contact_label' => 'Как с вами связаться',
|
||||
'form_index_contact_tab_email' => 'Email',
|
||||
'form_index_contact_tab_wa' => 'WhatsApp',
|
||||
'form_index_email_placeholder' => 'votre@email.com',
|
||||
'form_index_wa_placeholder' => '+33 6 00 00 00 00',
|
||||
|
||||
'form_index_submit' => 'Подобрать специалиста',
|
||||
'form_index_note' => 'Мы не передаём ваши данные третьим лицам и свяжемся с вами в течение нескольких часов.',
|
||||
|
||||
'form_index_success_title' => 'Заявка получена!',
|
||||
'form_index_success_text' => 'Мы изучим вашу ситуацию и свяжемся с вами в ближайшее время, чтобы предложить подходящего специалиста.',
|
||||
|
||||
'form_index_error_required' => 'Пожалуйста, заполните все обязательные поля.',
|
||||
'form_index_error_send' => 'Произошла ошибка при отправке. Пожалуйста, попробуйте ещё раз.',
|
||||
'form_index_error_popup_title' => 'Ошибка',
|
||||
|
||||
'regist_meta_title' => 'Регистрация специалиста на USLUGI.FR',
|
||||
'regist_meta_description' => 'Создайте профиль специалиста на USLUGI.FR и получайте новых клиентов во Франции.',
|
||||
'regist_meta_keywords' => 'специалист, регистрация, профиль, клиенты, Франция, консультации, uslugi',
|
||||
|
||||
'regist_tag' => 'Для специалистов',
|
||||
'regist_h1' => "Вы специалист?\nРазместите профиль на USLUGI.FR",
|
||||
'regist_hero_text' => 'Получайте новых клиентов во Франции!',
|
||||
'regist_btn_create_profile' => 'Создать профиль',
|
||||
|
||||
'regist_benefits_title' => 'Что вы получаете',
|
||||
|
||||
'regist_benefit_1_title' => 'Профиль с вашей информацией',
|
||||
'regist_benefit_1_text' => 'Специализация, город, фото, описание.',
|
||||
|
||||
'regist_benefit_2_title' => 'Календарь записи',
|
||||
'regist_benefit_2_text' => 'Клиент видит ваше свободное время и записывается сам.',
|
||||
|
||||
'regist_benefit_3_title' => 'Видеосвязь на платформе',
|
||||
'regist_benefit_3_text' => 'Онлайн-консультации без Zoom и лишних настроек.',
|
||||
|
||||
'regist_benefit_4_title' => 'Чат с клиентом',
|
||||
'regist_benefit_4_text' => 'Обменивайтесь документами и уточняйте детали до встречи.',
|
||||
|
||||
'regist_benefit_5_title' => 'Целевая аудитория',
|
||||
'regist_benefit_5_text' => 'Люди приходят с конкретным запросом. Часть записывается сами — часть мы направляем напрямую к вам через форму подбора.',
|
||||
|
||||
'regist_pricing_title' => 'Стоимость',
|
||||
'regist_pricing_sub' => 'Без комиссии с консультаций — оплата напрямую от клиента',
|
||||
'regist_pricing_badge' => 'Выгоднее',
|
||||
|
||||
'regist_yearly_label' => 'В год',
|
||||
'regist_yearly_price' => '275€',
|
||||
'regist_yearly_unit' => '23€ в месяц',
|
||||
|
||||
'regist_monthly_label' => 'В месяц',
|
||||
'regist_monthly_price' => '25€',
|
||||
'regist_monthly_unit' => '',
|
||||
|
||||
'regist_btn_choose' => 'Выбрать',
|
||||
'regist_pricing_note' => 'Оплата картой прямо на сайте через Stripe',
|
||||
|
||||
'regist_steps_title' => 'Как начать',
|
||||
|
||||
'regist_step_1_title' => 'Выберите подписку и оплатите картой',
|
||||
'regist_step_1_text' => 'Прямо на сайте, быстро и безопасно.',
|
||||
|
||||
'regist_step_2_title' => 'Создайте профиль',
|
||||
'regist_step_2_text' => 'Фото, описание, специализация, календарь — всё сами.',
|
||||
|
||||
'regist_step_3_title' => 'Получайте запросы от клиентов',
|
||||
'regist_step_3_text' => 'Уведомления на почту, запись в календарь, видеосвязь.',
|
||||
|
||||
'regist_help_title' => 'Нет времени заполнять профиль?',
|
||||
'regist_help_text' => 'После оплаты пришлите нам фото и информацию — мы оформим за вас.',
|
||||
|
||||
'regist_cta_title' => 'Готовы начать?',
|
||||
'regist_cta_text' => 'Регистрация занимает 5 минут',
|
||||
'label_mdp_actuel' => 'Текущий пароль',
|
||||
'placeholder_mdp_actuel' => 'Введите текущий пароль',
|
||||
'erreur_mdp_actuel_incorrect' => 'Неверный текущий пароль.',
|
||||
'trop_de_tentatives_connexion' => 'Слишком много попыток входа. Пожалуйста, попробуйте позже.',
|
||||
'verif_code_trop_tentatives' => 'Слишком много попыток. Пожалуйста, начните процедуру восстановления заново.',
|
||||
'mdp_oublie_message_neutre' => 'Если для этого адреса существует аккаунт, код был отправлен.',
|
||||
];
|
||||
File diff suppressed because it is too large.
Load diff
+38
-23
@@ -5,7 +5,7 @@ $success = $erreur = '';
|
||||
$id = $_SESSION['compte_id'];
|
||||
|
||||
// Récupérer e-mail actuel
|
||||
$stmt = $pdo->prepare("SELECT email FROM compte WHERE id = ?");
|
||||
$stmt = $pdo->prepare("SELECT email, mot_de_passe FROM compte WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$compte = $stmt->fetch();
|
||||
|
||||
@@ -16,22 +16,26 @@ if (!$compte) {
|
||||
$email_actuel = $compte['email'];
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
require_post_csrf();
|
||||
|
||||
$nouvel_email = trim($_POST['email'] ?? '');
|
||||
$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,9 +44,12 @@ 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
|
||||
$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'];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,25 +59,33 @@ 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>
|
||||
<div class="alert alert-danger"><?php echo htmlspecialchars($erreur) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (! empty($success)): ?>
|
||||
<div class="alert alert-success"><?= htmlspecialchars($success) ?></div>
|
||||
<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"><?= htmlspecialchars($translations['label_nouveau_mdp_opt']) ?></label>
|
||||
<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"><?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>
|
||||
|
||||
@@ -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,65 +78,59 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 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>
|
||||
<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') ?>
|
||||
<?php echo htmlspecialchars($translations['btn_ajouter_par_date'] ?? 'Ajouter par date'); ?>
|
||||
</a>
|
||||
</div>
|
||||
<div class="table-responsive mb-4">
|
||||
|
||||
<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>
|
||||
<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): ?>
|
||||
<?php if ($dispos): ?>
|
||||
<?php foreach ($dispos as $d): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<?php
|
||||
@@ -135,30 +138,38 @@ HTML;
|
||||
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><?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"
|
||||
<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') ?>
|
||||
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 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>
|
||||
<h4 class="mb-3 text-center"><?php echo htmlspecialchars($translations['card_ajouter_creneau'] ?? 'Ajouter un créneau'); ?></h4>
|
||||
|
||||
<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="jour"><?php echo htmlspecialchars($translations['label_jour'] ?? 'Jour') ?></label>
|
||||
<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>
|
||||
<option value=""><?php echo htmlspecialchars($translations['option_choisir'] ?? '— Choisir —'); ?></option>
|
||||
<?php
|
||||
$jours = [
|
||||
'lundi' => $translations['jour_lundi'] ?? 'Lundi',
|
||||
@@ -171,23 +182,23 @@ HTML;
|
||||
];
|
||||
foreach ($jours as $val => $label):
|
||||
?>
|
||||
<option value="<?php echo htmlspecialchars($val) ?>"><?php echo htmlspecialchars($label) ?></option>
|
||||
<option value="<?php echo htmlspecialchars($val); ?>"><?php echo htmlspecialchars($label); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="heure_debut"><?php echo htmlspecialchars($translations['label_heure_debut'] ?? 'Heure de début') ?></label>
|
||||
<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>
|
||||
<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') ?>
|
||||
<?php echo htmlspecialchars($translations['btn_ajouter'] ?? 'Ajouter'); ?>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -196,34 +207,33 @@ HTML;
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// --- SweetAlert2: messages succès / erreur depuis PHP ---
|
||||
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é.") ?>',
|
||||
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") ?>',
|
||||
title: <?php echo json_encode($translations['erreur_titre'] ?? 'Erreur', JSON_UNESCAPED_UNICODE); ?>,
|
||||
html: erreurMsg,
|
||||
showCancelButton: true,
|
||||
confirmButtonText: '<?php echo addslashes($translations["btn_modifier_profil"] ?? "Modifier mon profil") ?>',
|
||||
confirmButtonText: <?php echo json_encode($translations['btn_modifier_profil'] ?? 'Modifier mon profil', JSON_UNESCAPED_UNICODE); ?>,
|
||||
cancelButtonText: 'OK'
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
@@ -233,30 +243,73 @@ document.addEventListener('DOMContentLoaded', function(){
|
||||
}
|
||||
});
|
||||
|
||||
// --- Confirmation de suppression avec SweetAlert2 ---
|
||||
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,14 +322,19 @@ 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>
|
||||
|
||||
@@ -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,44 +85,34 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 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
|
||||
// ---- 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"
|
||||
);
|
||||
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>
|
||||
<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 class="btn btn-outline-secondary" href="/public/praticien/disponibilites.php">
|
||||
<?php echo htmlspecialchars($translations['retour_au_planning'] ?? 'Retour'); ?>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -117,58 +120,64 @@ HTML;
|
||||
<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>
|
||||
<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): ?>
|
||||
<?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>
|
||||
<?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"
|
||||
<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') ?>
|
||||
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 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>
|
||||
<h4 class="mb-3 text-center">
|
||||
<?php echo htmlspecialchars($translations['card_ajouter_creneau_date'] ?? 'Ajouter une disponibilité ponctuelle'); ?>
|
||||
</h4>
|
||||
|
||||
<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="date_dispo"><?php echo htmlspecialchars($translations['cl_date_rdv'] ?? 'Date') ?></label>
|
||||
<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_debut"><?php echo htmlspecialchars($translations['label_heure_debut'] ?? 'Heure de début') ?></label>
|
||||
<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>
|
||||
<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') ?>
|
||||
<?php echo htmlspecialchars($translations['btn_ajouter'] ?? 'Ajouter'); ?>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -177,23 +186,32 @@ HTML;
|
||||
</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") ?>',
|
||||
title: <?php echo json_encode($translations["erreur_titre"] ?? "Erreur", JSON_UNESCAPED_UNICODE); ?>,
|
||||
html: erreurMsg,
|
||||
showCancelButton: true,
|
||||
confirmButtonText: '<?php echo addslashes($translations["btn_modifier_profil"] ?? "Modifier mon profil") ?>',
|
||||
confirmButtonText: <?php echo json_encode($translations["btn_modifier_profil"] ?? "Modifier mon profil", JSON_UNESCAPED_UNICODE); ?>,
|
||||
cancelButtonText: 'OK'
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
@@ -206,24 +224,70 @@ document.addEventListener('DOMContentLoaded', function(){
|
||||
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,13 +300,19 @@ 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>
|
||||
|
||||
@@ -1,86 +1,113 @@
|
||||
<?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';
|
||||
}
|
||||
|
||||
// Récupérer les infos du praticien
|
||||
// Base praticien
|
||||
$stmt = $pdo->prepare("SELECT * FROM praticien WHERE id = ?");
|
||||
$stmt->execute([$_SESSION['reference_id'] ?? 0]);
|
||||
$stmt->execute([$id]);
|
||||
$praticien = $stmt->fetch();
|
||||
|
||||
$id = $_SESSION['reference_id'] ?? null;
|
||||
if (!$id) { http_response_code(403); exit('Forbidden'); }
|
||||
if (! $praticien) {
|
||||
http_response_code(404);
|
||||
exit(htmlspecialchars($translations['praticien_introuvable'] ?? 'Introuvable.'));
|
||||
}
|
||||
|
||||
// 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>
|
||||
<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.
|
||||
?>
|
||||
+434
-115
@@ -1,23 +1,284 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../includes/init_praticien.php';
|
||||
|
||||
|
||||
$id = $_SESSION['reference_id'] ?? null;
|
||||
if (!$id) { http_response_code(403); exit('Forbidden'); }
|
||||
if (! $id) {
|
||||
http_response_code(403);
|
||||
exit('Forbidden');
|
||||
}
|
||||
|
||||
// Récupérer les données (pour affichage initial & valeurs par défaut)
|
||||
$currentLang = $lang ?? 'ru';
|
||||
if (! in_array($currentLang, ['ru', 'fr', 'en'], true)) {
|
||||
$currentLang = 'ru';
|
||||
}
|
||||
$isRu = ($currentLang === 'ru');
|
||||
|
||||
$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 ====
|
||||
// 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'] ?? '');
|
||||
@@ -28,37 +289,47 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$disponibilite = trim($_POST['disponibilite'] ?? '');
|
||||
$photo = $praticien['photo'] ?? null;
|
||||
|
||||
if ($specialite === '' || $presentation === '') {
|
||||
die(htmlspecialchars($translations['err_champs_obligatoires'] ?? 'Merci de remplir les champs obligatoires.'));
|
||||
}
|
||||
|
||||
// --- BLOC UPLOAD SÉCURISÉ ---
|
||||
if (!empty($_FILES['photo']['tmp_name'])) {
|
||||
// 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; // 5 Mo
|
||||
$maxSize = 5 * 1024 * 1024;
|
||||
|
||||
// 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']);
|
||||
$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;
|
||||
|
||||
// 4. Traitement de l'image (Conversion & Redimensionnement)
|
||||
$image_source = @imagecreatefromstring(file_get_contents($file['tmp_name']));
|
||||
if (! $image_source) {
|
||||
die("Erreur : Le fichier image semble corrompu.");
|
||||
}
|
||||
|
||||
if ($image_source) {
|
||||
list($largeur_orig, $hauteur_orig) = getimagesize($file['tmp_name']);
|
||||
[$largeur_orig, $hauteur_orig] = getimagesize($file['tmp_name']);
|
||||
$largeur_max = 800;
|
||||
|
||||
if ($largeur_orig > $largeur_max) {
|
||||
@@ -74,11 +345,17 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
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);
|
||||
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)) {
|
||||
@@ -90,38 +367,46 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
|
||||
imagedestroy($image_source);
|
||||
imagedestroy($image_dest);
|
||||
} else {
|
||||
die("Erreur : Le fichier image semble corrompu.");
|
||||
}
|
||||
}
|
||||
|
||||
// SIRET - normalisation + vérifs
|
||||
$pdo->beginTransaction();
|
||||
|
||||
try {
|
||||
if ($isRu) {
|
||||
$siret = preg_replace('/\D/', '', $_POST['siret'] ?? '');
|
||||
|
||||
if (strlen($siret) !== 14) {
|
||||
die(htmlspecialchars($translations['siret_format_invalide'] ?? 'SIRET invalide (14 chiffres).'));
|
||||
throw new RuntimeException($translations['siret_format_invalide'] ?? 'SIRET invalide (14 chiffres).');
|
||||
}
|
||||
// Luhn
|
||||
|
||||
$luhnOk = (function ($num) {
|
||||
$sum=0; $alt=false;
|
||||
$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;
|
||||
if ($alt) {
|
||||
$n *= 2;
|
||||
if ($n > 9) {
|
||||
$n -= 9;
|
||||
}
|
||||
|
||||
}
|
||||
$sum += $n;
|
||||
$alt = ! $alt;
|
||||
}
|
||||
return $sum % 10 === 0;
|
||||
})($siret);
|
||||
|
||||
if (! $luhnOk) {
|
||||
die(htmlspecialchars($translations['siret_format_invalide'] ?? 'SIRET invalide (Luhn).'));
|
||||
throw new RuntimeException($translations['siret_format_invalide'] ?? 'SIRET invalide (Luhn).');
|
||||
}
|
||||
|
||||
// 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é.'));
|
||||
throw new RuntimeException($translations['err_siret_deja_utilise'] ?? 'Ce SIRET est déjà utilisé.');
|
||||
}
|
||||
|
||||
// Mise à jour
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE praticien SET
|
||||
titre = :titre,
|
||||
@@ -136,13 +421,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
photo = :photo,
|
||||
disponibilite = :disponibilite,
|
||||
duree_rdv = :duree_rdv,
|
||||
siret = :siret
|
||||
siret = :siret,
|
||||
langue_source = 'ru'
|
||||
WHERE id = :id
|
||||
");
|
||||
$stmt->execute([
|
||||
'titre' => $titre,
|
||||
'nom' => $nom,
|
||||
'prenom' => $prenom,
|
||||
'nom' => $nomAffiche,
|
||||
'prenom' => $prenomAffiche,
|
||||
'telephone' => $telephone,
|
||||
'specialite' => $specialite,
|
||||
'adresse' => $adresse,
|
||||
@@ -156,7 +442,37 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
'id' => $id,
|
||||
]);
|
||||
|
||||
// Redirection ABSOLUE (ou "informations.php" si même dossier)
|
||||
// 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()));
|
||||
}
|
||||
|
||||
header('Location: /public/praticien/informations.php');
|
||||
exit;
|
||||
}
|
||||
@@ -172,118 +488,130 @@ $HEAD_EXTRA = <<<HTML
|
||||
}
|
||||
</style>
|
||||
HTML;
|
||||
// ==== À partir d’ici, on peut afficher ====
|
||||
|
||||
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"
|
||||
|
||||
<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="<?=htmlspecialchars($translations['telephone_format_message']?? '') ?>">
|
||||
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>
|
||||
<label class="form-label"><?php echo htmlspecialchars($translations['placeholder_siret']) ?> *</label>
|
||||
<input type="text"
|
||||
name="siret"
|
||||
id="siret"
|
||||
class="form-control"
|
||||
value="<?= htmlspecialchars($praticien['siret']?? '') ?>"
|
||||
value="<?php echo htmlspecialchars($praticien['siret'] ?? '') ?>"
|
||||
required
|
||||
maxlength="17"
|
||||
inputmode="numeric"
|
||||
pattern="[0-9 ]+">
|
||||
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['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['tarif_consultation']) ?></label>
|
||||
<input type="text" name="titre" class="form-control" value="<?= htmlspecialchars($praticien['titre']?? '') ?>">
|
||||
<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"><?= htmlspecialchars($translations['moyens_payement']) ?></label>
|
||||
<input type="text" name="disponibilite" class="form-control" value="<?= htmlspecialchars($praticien['disponibilite']?? '') ?>">
|
||||
<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">
|
||||
<p><?= htmlspecialchars($translations['photo_actuelle']) ?></p>
|
||||
<img src="/public/uploads/<?= htmlspecialchars($praticien['photo']) ?>" class="img-fluid rounded-4 shadow" style="width:230px;height:auto;">
|
||||
<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><?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>
|
||||
|
||||
<button type="submit" class="btn btn-primary"><?php echo htmlspecialchars($translations['btn_enregistrer']) ?></button>
|
||||
</div>
|
||||
<?= $translations['champs_obligatoire'] ?>
|
||||
|
||||
</div>
|
||||
<?php echo $translations['champs_obligatoire'] ?>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<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()) {
|
||||
@@ -308,19 +635,20 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
const cpInput = document.getElementById("code_postal");
|
||||
const suggestions = document.getElementById("suggestions");
|
||||
|
||||
// petite fonction utilitaire : construit "numero + rue"
|
||||
if (adresseInput.hasAttribute('readonly')) {
|
||||
return;
|
||||
}
|
||||
|
||||
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
|
||||
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);
|
||||
|
||||
+282
-59
@@ -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,11 +160,7 @@
|
||||
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, ...)
|
||||
// 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;
|
||||
@@ -120,35 +223,35 @@
|
||||
$params = [];
|
||||
|
||||
/*========================== BLOCK WHERE AVEC ABONNEMENT VALIDE ==========================*/
|
||||
$where[] = "(date_expiration IS NOT NULL AND date_expiration >= DATE_SUB(NOW(), INTERVAL 14 DAY))";
|
||||
/*=======================================================================================*/
|
||||
$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
|
||||
// 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";
|
||||
$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);
|
||||
$stmt->execute($params);
|
||||
|
||||
$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') ?>">
|
||||
<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">
|
||||
@@ -343,23 +566,23 @@ 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="Фото специалиста">
|
||||
<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 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'] ?>">
|
||||
|
||||
@@ -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;
|
||||
+85
-13
@@ -1,9 +1,10 @@
|
||||
<?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',
|
||||
@@ -11,6 +12,7 @@ $UGLY_TO_PRETTY = [
|
||||
'/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',
|
||||
@@ -22,7 +24,7 @@ $UGLY_TO_PRETTY = [
|
||||
'/public/client/reserver_rdv.php' => '/reservation',
|
||||
'/public/client/mes_rendez_vous.php' => '/mes-rendez-vous',
|
||||
|
||||
//Dossier praticien
|
||||
// dossier praticien
|
||||
'/public/praticien/informations.php' => '/profil-pro',
|
||||
'/public/praticien/modifier.php' => '/modifier-profil-pro',
|
||||
'/public/praticien/compte.php' => '/mon-compte-pro',
|
||||
@@ -34,23 +36,98 @@ $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, '/');
|
||||
|
||||
// =========================
|
||||
// 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)) {
|
||||
@@ -59,11 +136,6 @@ if (!is_file($targetAbsolute)) {
|
||||
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>';
|
||||
+2
-2
@@ -8,8 +8,8 @@ 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',
|
||||
'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
|
||||
|
||||
@@ -3,8 +3,8 @@ 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',
|
||||
'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
|
||||
|
||||
@@ -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>';
|
||||
+10
-47
@@ -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(
|
||||
@@ -160,7 +156,9 @@ try {
|
||||
UPDATE compte
|
||||
SET email_valide = 1,
|
||||
token_validation = NULL,
|
||||
token_validation_expire = NULL
|
||||
token_validation_expire = NULL,
|
||||
token_autologin = NULL,
|
||||
token_autologin_expire = NULL
|
||||
WHERE id = :id
|
||||
");
|
||||
$stmt->execute(['id' => $compte['id']]);
|
||||
@@ -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);
|
||||
|
||||
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'];
|
||||
$_SESSION['compte_id'] = (int) $compte['id'];
|
||||
$_SESSION['role'] = (string) $compte['role'];
|
||||
$_SESSION['reference_id'] = (int) $compte['reference_id'];
|
||||
|
||||
if ($_SESSION['role'] === 'client') {
|
||||
$_SESSION['client_id'] = (int) $c['reference_id'];
|
||||
$_SESSION['client_id'] = (int) $compte['reference_id'];
|
||||
header('Location: /public/client/informations.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SESSION['role'] === 'praticien') {
|
||||
$_SESSION['praticien_id'] = (int) $c['reference_id'];
|
||||
$_SESSION['praticien_id'] = (int) $compte['reference_id'];
|
||||
header('Location: /public/praticien/informations.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// fallback si role inconnu
|
||||
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