Version stable, aprelic, connexion CFRS et autres corrections

This commit is contained in:
Oleg Barykin committed 2026-04-17 20:18:11 +02:00
1 parent 865de8a976
commit e9d0010451
62 files changed
+17483 -7256

No files matched your search

+307 -190
View File
@@ -1,248 +1,358 @@
<?php
// /public/cabinet/disponibilites.php (VERSION SECRÉTAIRE)
require_once __DIR__ . '/../includes/init_secretaire.php';
declare (strict_types = 1);
// /public/cabinet/disponibilites.php
require_once __DIR__ . '/../includes/init_secretaire.php';
// --- Helpers
if (!function_exists('h')) { function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); } }
if (! function_exists('h')) {
function h($s): string
{
return htmlspecialchars((string) $s, ENT_QUOTES, 'UTF-8');
}
}
// --- Contexte praticien obligatoire (depuis le lien du dashboard)
$compteId = (int)($_SESSION['compte_id'] ?? 0);
$userRole = strtolower((string)($_SESSION['role'] ?? ''));
$praticien_id = isset($_GET['praticien_id']) ? (int)$_GET['praticien_id'] : 0;
if ($praticien_id <= 0 || $userRole !== 'secretaire') { http_response_code(403); exit('Forbidden'); }
$compteId = (int) ($_SESSION['compte_id'] ?? 0);
$userRole = strtolower((string) ($_SESSION['role'] ?? ''));
$praticien_id = (int) ($_GET['praticien_id'] ?? 0);
// --- Vérifier pivot + droit can_edit_slots
$st = $pdo->prepare("
SELECT can_edit_slots
if ($praticien_id <= 0 || $userRole !== 'secretaire') {
http_response_code(403);
exit('Forbidden');
}
// Vérifier pivot + droit
$st = $pdo->prepare("
SELECT can_edit_slots
FROM secretaire_praticien
WHERE secretaire_user_id = :u
AND praticien_id = :p
AND is_active = 1
LIMIT 1
WHERE secretaire_user_id = :u
AND praticien_id = :p
AND is_active = 1
LIMIT 1
");
$st->execute(['u'=>$compteId,'p'=>$praticien_id]);
$pivot = $st->fetch(PDO::FETCH_ASSOC);
if (!$pivot || (int)$pivot['can_edit_slots'] !== 1) { http_response_code(403); exit('Forbidden'); }
$st->execute([
'u' => $compteId,
'p' => $praticien_id,
]);
$pivot = $st->fetch(PDO::FETCH_ASSOC);
// ---- Ajouter un créneau (POST)
$erreur = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['jour'], $_POST['heure_debut'], $_POST['heure_fin'])) {
$jour = trim((string)$_POST['jour']);
$debut = trim((string)$_POST['heure_debut']);
$fin = trim((string)$_POST['heure_fin']);
if (! $pivot || (int) $pivot['can_edit_slots'] !== 1) {
http_response_code(403);
exit('Forbidden');
}
// mini validation
$joursValides = ['lundi','mardi','mercredi','jeudi','vendredi','samedi','dimanche'];
if (!in_array($jour, $joursValides, true)) {
$erreur = $translations['erreur_jour_invalide'] ?? "Jour invalide.";
} elseif (!preg_match('#^\d{2}:\d{2}$#', $debut) || !preg_match('#^\d{2}:\d{2}$#', $fin)) {
$erreur = $translations['erreur_format_heure'] ?? "Format d’heure invalide.";
} elseif ($debut >= $fin) {
// Récup nom praticien pour le titre
$st = $pdo->prepare("SELECT prenom, nom, duree_rdv FROM praticien WHERE id = ? LIMIT 1");
$st->execute([$praticien_id]);
$praticien = $st->fetch(PDO::FETCH_ASSOC);
if (! $praticien) {
http_response_code(404);
exit('Praticien introuvable');
}
$currentPraticienName = trim(((string) ($praticien['prenom'] ?? '')) . ' ' . ((string) ($praticien['nom'] ?? '')));
$duree_minutes = (int) ($praticien['duree_rdv'] ?? 0);
if ($duree_minutes <= 0) {
$duree_minutes = 30;
}
$erreur = null;
$toMin = function (string $hhmm): ?int {
if (! preg_match('/^\d{2}:\d{2}$/', $hhmm)) {
return null;
}
[$h, $m] = array_map('intval', explode(':', $hhmm));
if ($h < 0 || $h > 23 || $m < 0 || $m > 59) {
return null;
}
return $h * 60 + $m;
};
// ---- Ajouter un créneau (POST + CSRF)
if (
$_SERVER['REQUEST_METHOD'] === 'POST'
&& ($_POST['action'] ?? '') === 'add'
&& isset($_POST['jour'], $_POST['heure_debut'], $_POST['heure_fin'])
) {
require_post_csrf();
$jour = trim((string) $_POST['jour']);
$debut = trim((string) $_POST['heure_debut']);
$fin = trim((string) $_POST['heure_fin']);
$joursValides = ['lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi', 'dimanche'];
$md = $toMin($debut);
$mf = $toMin($fin);
if (! in_array($jour, $joursValides, true)) {
$erreur = $translations['erreur_jour_invalide'] ?? 'Jour invalide.';
} elseif ($md === null || $mf === null) {
$erreur = $translations['erreur_format_heure'] ?? "Format d'heure invalide.";
} elseif ($md >= $mf) {
$erreur = $translations['erreur_debut_avant_fin'] ?? "Heure de début après l’heure de fin.";
}
if (!$erreur) {
$stmt = $pdo->prepare(
"INSERT INTO disponibilite (praticien_id, jour_semaine, heure_debut, heure_fin)
VALUES (:p, :j, :hd, :hf)"
);
$stmt->execute([
'p'=>$praticien_id,
'j'=>$jour,
'hd'=>$debut,
'hf'=>$fin
]);
header('Location: /public/cabinet/disponibilites.php?praticien_id='.$praticien_id.'&ok=added', true, 303);
exit;
}
}
// ---- Supprimer un créneau (GET)
// (idéalement: POST + CSRF, on garde GET pour coller à l’original, avec une vérif d’appartenance)
if (isset($_GET['supprimer'])) {
$id = (int)$_GET['supprimer'];
// Vérifier que ce slot appartient bien à ce praticien
$own = $pdo->prepare("SELECT 1 FROM disponibilite WHERE id = :id AND praticien_id = :p LIMIT 1");
$own->execute(['id'=>$id,'p'=>$praticien_id]);
if ($own->fetchColumn()) {
$stmt = $pdo->prepare("DELETE FROM disponibilite WHERE id = :id AND praticien_id = :p");
$stmt->execute(['id'=>$id,'p'=>$praticien_id]);
header('Location: /public/cabinet/disponibilites.php?praticien_id='.$praticien_id.'&ok=deleted', true, 303);
exit;
} else {
http_response_code(403); exit('Forbidden');
$window = $mf - $md;
if ($window < $duree_minutes) {
$erreur = sprintf(
$translations['erreur_plage_trop_courte'] ?? "Créneau trop court (%d min). Minimum : %d min.",
$window,
$duree_minutes
);
} elseif (($window % $duree_minutes) !== 0) {
$erreur = sprintf(
$translations['erreur_plage_incompatible_duree'] ?? "Créneau incompatible. La durée doit être un multiple de %d min.",
$duree_minutes
);
} else {
$stmt = $pdo->prepare("
INSERT INTO disponibilite (praticien_id, jour_semaine, heure_debut, heure_fin)
VALUES (:p, :j, :hd, :hf)
");
$stmt->execute([
'p' => $praticien_id,
'j' => $jour,
'hd' => $debut,
'hf' => $fin,
]);
header('Location: /public/cabinet/disponibilites.php?praticien_id=' . $praticien_id . '&ok=added', true, 303);
exit;
}
}
}
}
// ---- Récupérer les créneaux
$stmt = $pdo->prepare(
"SELECT * FROM disponibilite
WHERE praticien_id = ?
ORDER BY FIELD(jour_semaine,'lundi','mardi','mercredi','jeudi','vendredi','samedi','dimanche'), heure_debut"
);
$stmt->execute([$praticien_id]);
$dispos = $stmt->fetchAll(PDO::FETCH_ASSOC);
// ---- Récupérer les créneaux
$stmt = $pdo->prepare("
SELECT *
FROM disponibilite
WHERE praticien_id = ?
ORDER BY FIELD(jour_semaine,'lundi','mardi','mercredi','jeudi','vendredi','samedi','dimanche'), heure_debut
");
$stmt->execute([$praticien_id]);
$dispos = $stmt->fetchAll(PDO::FETCH_ASSOC);
$jourKeyMap = [
'lundi' => 'jour_lundi',
'mardi' => 'jour_mardi',
'mercredi' => 'jour_mercredi',
'jeudi' => 'jour_jeudi',
'vendredi' => 'jour_vendredi',
'samedi' => 'jour_samedi',
'dimanche' => 'jour_dimanche',
];
// Prépare les traductions de jours
$jourKeyMap = [
'lundi'=>'jour_lundi','mardi'=>'jour_mardi','mercredi'=>'jour_mercredi',
'jeudi'=>'jour_jeudi','vendredi'=>'jour_vendredi','samedi'=>'jour_samedi','dimanche'=>'jour_dimanche',
];
$PAGE_TITRE = $translations['titre_disponibilites_date'] ?? 'Disponibilités (dates)';
$HEAD_EXTRA = <<<HTML
<script src="/js/sweetalert2@11.js"></script>
$PAGE_TITLE = $translations['titre_disponibilites'] ?? 'Disponibilités';
$HEAD_EXTRA = <<<HTML
<script src="/js/sweetalert2@11.js"></script>
HTML;
require_once __DIR__ . '/../includes/header_cabinet.php';
require_once __DIR__ . '/../includes/header_cabinet.php';
?>
<div class="main1">
<div class="container py-5">
<div class="shadow p-4 rounded bg-light">
<h2 class="mb-4 text-center"><?= htmlspecialchars($currentPraticienName, ENT_QUOTES, 'UTF-8') ?> - <?= h($translations['sloti'] ?? 'Mes disponibilités') ?></h2>
<div class="text-end">
<a href="/public/cabinet/disponibilites_date.php?praticien_id=<?= (int)$praticien_id ?>" class="btn btn-outline-secondary mb-4">
<?= htmlspecialchars($translations['btn_ajouter_par_date'] ?? 'Ajouter par date') ?>
</a>
</div>
<div class="table-responsive mb-4">
<table class="table table-bordered table-striped align-middle text-center mobile-cards" id="table_rdv">
<thead class="table-light">
<tr>
<th><?= h($translations['table_jour'] ?? 'Jour') ?></th>
<th><?= h($translations['table_debut'] ?? 'Début') ?></th>
<th><?= h($translations['table_fin'] ?? 'Fin') ?></th>
<th><?= h($translations['table_action'] ?? 'Action') ?></th>
</tr>
</thead>
<tbody>
<?php if ($dispos): foreach ($dispos as $d): ?>
<tr>
<td>
<?php
$key = $jourKeyMap[$d['jour_semaine']] ?? null;
echo h($key ? ($translations[$key] ?? ucfirst($d['jour_semaine'])) : ucfirst($d['jour_semaine']));
?>
</td>
<td><?= h(substr((string)$d['heure_debut'], 0, 5)) ?></td>
<td><?= h(substr((string)$d['heure_fin'], 0, 5)) ?></td>
<td>
<button type="button"
<div class="container py-5">
<div class="shadow p-4 rounded bg-light">
<h2 class="mb-4 text-center">
<?php echo h($currentPraticienName) ?> - <?php echo h($translations['sloti'] ?? 'Mes disponibilités') ?>
</h2>
<div class="text-end">
<a href="/public/cabinet/disponibilites_date.php?praticien_id=<?php echo (int) $praticien_id ?>" class="btn btn-outline-secondary mb-4">
<?php echo h($translations['btn_ajouter_par_date'] ?? 'Ajouter par date') ?>
</a>
</div>
<div class="table-responsive mb-4">
<table class="table table-bordered table-striped align-middle text-center mobile-cards" id="table_rdv">
<thead class="table-light">
<tr>
<th><?php echo h($translations['table_jour'] ?? 'Jour') ?></th>
<th><?php echo h($translations['table_debut'] ?? 'Début') ?></th>
<th><?php echo h($translations['table_fin'] ?? 'Fin') ?></th>
<th><?php echo h($translations['table_action'] ?? 'Action') ?></th>
</tr>
</thead>
<tbody>
<?php if ($dispos): ?>
<?php foreach ($dispos as $d): ?>
<tr>
<td>
<?php
$key = $jourKeyMap[$d['jour_semaine']] ?? null;
echo h($key ? ($translations[$key] ?? ucfirst($d['jour_semaine'])) : ucfirst($d['jour_semaine']));
?>
</td>
<td><?php echo h(substr((string) $d['heure_debut'], 0, 5)) ?></td>
<td><?php echo h(substr((string) $d['heure_fin'], 0, 5)) ?></td>
<td>
<button
type="button"
class="btn btn-sm btn-danger js-del"
data-id="<?= (int)$d['id'] ?>">
<?= h($translations['btn_supprimer'] ?? 'Supprimer') ?>
</button>
</td>
</tr>
<?php endforeach; else: ?>
<tr><td colspan="4"><?= h($translations['aucun_creneau'] ?? 'Aucun créneau.') ?></td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
data-id="<?php echo (int) $d['id'] ?>">
<?php echo h($translations['btn_supprimer'] ?? 'Supprimer') ?>
</button>
</td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="4"><?php echo h($translations['aucun_creneau'] ?? 'Aucun créneau.') ?></td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<div class="card shadow-sm p-4">
<h4 class="mb-3 text-center"><?= h($translations['card_ajouter_creneau'] ?? 'Ajouter un créneau') ?></h4>
<form method="POST">
<input type="hidden" name="praticien_id" value="<?= (int)$praticien_id ?>">
<div class="mb-3">
<label class="form-label" for="jour"><?= h($translations['label_jour'] ?? 'Jour') ?></label>
<select name="jour" id="jour" class="form-select" required>
<option value=""><?= h($translations['option_choisir'] ?? '— Choisir —') ?></option>
<?php
$jours = [
'lundi' => $translations['jour_lundi'] ?? 'Lundi',
'mardi' => $translations['jour_mardi'] ?? 'Mardi',
'mercredi' => $translations['jour_mercredi'] ?? 'Mercredi',
'jeudi' => $translations['jour_jeudi'] ?? 'Jeudi',
'vendredi' => $translations['jour_vendredi'] ?? 'Vendredi',
'samedi' => $translations['jour_samedi'] ?? 'Samedi',
'dimanche' => $translations['jour_dimanche'] ?? 'Dimanche',
];
foreach ($jours as $val => $label):
?>
<option value="<?= h($val) ?>"><?= h($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="card shadow-sm p-4">
<h4 class="mb-3 text-center"><?php echo h($translations['card_ajouter_creneau'] ?? 'Ajouter un créneau') ?></h4>
<div class="mb-3">
<label class="form-label" for="heure_debut"><?= h($translations['label_heure_debut'] ?? 'Heure de début') ?></label>
<input type="time" name="heure_debut" id="heure_debut" class="form-control" required>
</div>
<form method="POST">
<input type="hidden" name="action" value="add">
<input type="hidden" name="csrf_token" value="<?php echo h(ensure_csrf_token()) ?>">
<div class="mb-3">
<label class="form-label" for="heure_fin"><?= h($translations['label_heure_fin'] ?? 'Heure de fin') ?></label>
<input type="time" name="heure_fin" id="heure_fin" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label" for="jour"><?php echo h($translations['label_jour'] ?? 'Jour') ?></label>
<select name="jour" id="jour" class="form-select" required>
<option value=""><?php echo h($translations['option_choisir'] ?? '— Choisir —') ?></option>
<?php
$jours = [
'lundi' => $translations['jour_lundi'] ?? 'Lundi',
'mardi' => $translations['jour_mardi'] ?? 'Mardi',
'mercredi' => $translations['jour_mercredi'] ?? 'Mercredi',
'jeudi' => $translations['jour_jeudi'] ?? 'Jeudi',
'vendredi' => $translations['jour_vendredi'] ?? 'Vendredi',
'samedi' => $translations['jour_samedi'] ?? 'Samedi',
'dimanche' => $translations['jour_dimanche'] ?? 'Dimanche',
];
foreach ($jours as $val => $label):
?>
<option value="<?php echo h($val) ?>"><?php echo h($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<button type="submit" class="btn btn-primary w-100">
<?= h($translations['btn_ajouter'] ?? 'Ajouter') ?>
</button>
</form>
<div class="mb-3">
<label class="form-label" for="heure_debut"><?php echo h($translations['label_heure_debut'] ?? 'Heure de début') ?></label>
<input type="time" name="heure_debut" id="heure_debut" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label" for="heure_fin"><?php echo h($translations['label_heure_fin'] ?? 'Heure de fin') ?></label>
<input type="time" name="heure_fin" id="heure_fin" class="form-control" required>
</div>
<button type="submit" class="btn btn-primary w-100">
<?php echo h($translations['btn_ajouter'] ?? 'Ajouter') ?>
</button>
</form>
</div>
</div>
</div>
</div>
</div>
<script>
// --- SweetAlert2: messages succès / erreur depuis PHP ---
document.addEventListener('DOMContentLoaded', function(){
const okParam = '<?= isset($_GET['ok']) ? addslashes((string)$_GET['ok']) : '' ?>';
const erreurMsg = '<?= $erreur ? addslashes((string)$erreur) : '' ?>';
const csrfToken = <?php echo json_encode(ensure_csrf_token(), JSON_UNESCAPED_UNICODE) ?>;
const praticienId = <?php echo (int) $praticien_id ?>;
document.addEventListener('DOMContentLoaded', function () {
const okParam = <?php echo json_encode(isset($_GET['ok']) ? (string) $_GET['ok'] : '', JSON_UNESCAPED_UNICODE) ?>;
const erreurMsg = <?php echo json_encode($erreur ?: '', JSON_UNESCAPED_UNICODE) ?>;
if (okParam === 'added') {
Swal.fire({
icon: 'success',
title: '<?= addslashes($translations["msg_creneau_ajoute"] ?? "Créneau ajouté.") ?>',
timer: 1800,
showConfirmButton: false
});
} else if (okParam === 'deleted') {
Swal.fire({
icon: 'success',
title: '<?= addslashes($translations["msg_creneau_supprime"] ?? "Créneau supprimé.") ?>',
title: <?php echo json_encode($translations['msg_creneau_ajoute'] ?? 'Créneau ajouté.', JSON_UNESCAPED_UNICODE) ?>,
timer: 1800,
showConfirmButton: false
});
if (window.history.replaceState) {
window.history.replaceState({}, document.title, window.location.pathname + '?praticien_id=' + encodeURIComponent(praticienId));
}
}
if (erreurMsg) {
Swal.fire({
icon: 'error',
title: '<?= addslashes($translations["erreur_titre"] ?? "Erreur") ?>',
title: <?php echo json_encode($translations['erreur_titre'] ?? 'Erreur', JSON_UNESCAPED_UNICODE) ?>,
text: erreurMsg,
confirmButtonText: 'OK'
});
}
});
// --- Confirmation de suppression avec SweetAlert2 ---
document.addEventListener('click', async function(e){
document.addEventListener('click', async function (e) {
const btn = e.target.closest('.js-del');
if (!btn) return;
const id = btn.dataset.id;
if (!id) return;
const result = await Swal.fire({
icon: 'warning',
title: '<?= addslashes($translations["confirm_title"] ?? "Confirmation") ?>',
text: '<?= addslashes($translations["confirm_supprimer_creneau"] ?? "Supprimer ce créneau ?") ?>',
title: <?php echo json_encode($translations['confirm_title'] ?? 'Confirmation', JSON_UNESCAPED_UNICODE) ?>,
text: <?php echo json_encode($translations['confirm_supprimer_creneau'] ?? 'Supprimer ce créneau ?', JSON_UNESCAPED_UNICODE) ?>,
showCancelButton: true,
confirmButtonText: '<?= addslashes($translations["btn_da_udalit"] ?? "Oui") ?>',
cancelButtonText: '<?= addslashes($translations["btn_annuler"] ?? "Annuler") ?>',
confirmButtonText: <?php echo json_encode($translations['btn_da_udalit'] ?? 'Oui', JSON_UNESCAPED_UNICODE) ?>,
cancelButtonText: <?php echo json_encode($translations['btn_annuler'] ?? 'Annuler', JSON_UNESCAPED_UNICODE) ?>,
reverseButtons: true
});
if (result.isConfirmed) {
// GET comme dans la page d’origine, on garde le contexte praticien_id
const pid = '<?= (int)$praticien_id ?>';
window.location.href = '?praticien_id=' + encodeURIComponent(pid) + '&supprimer=' + encodeURIComponent(id);
}
if (!result.isConfirmed) return;
const formData = new FormData();
formData.append('id', String(id));
formData.append('praticien_id', String(praticienId));
formData.append('csrf_token', csrfToken);
fetch('/public/cabinet/supprimer_disponibilite.php', {
method: 'POST',
headers: {
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: formData
})
.then(async r => {
const text = await r.text();
try {
return JSON.parse(text);
} catch (e) {
return { ok: false, message: text };
}
})
.then(data => {
if (data.ok) {
Swal.fire({
icon: 'success',
title: <?php echo json_encode($translations['msg_creneau_supprime'] ?? 'Créneau supprimé.', JSON_UNESCAPED_UNICODE) ?>,
timer: 1400,
showConfirmButton: false
}).then(() => {
window.location.reload();
});
} else {
Swal.fire({
icon: 'error',
title: <?php echo json_encode($translations['erreur_titre'] ?? 'Erreur', JSON_UNESCAPED_UNICODE) ?>,
text: data.message || 'Suppression impossible.'
});
}
})
.catch(() => {
Swal.fire({
icon: 'error',
title: 'Erreur réseau',
text: 'Veuillez réessayer.'
});
});
});
</script>
<!--*********Adaptation des tables pour un affichage mobile***************-->
<script defer>
(function(){
function initMobileCards(tbl){
@@ -255,15 +365,22 @@ document.addEventListener('click', async function(e){
});
document.documentElement.classList.add('mc-ready');
}
function ready(fn){ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fn); else fn(); }
ready(function(){ initMobileCards(document.getElementById('table_rdv')); });
function ready(fn){
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fn);
else fn();
}
ready(function(){
initMobileCards(document.getElementById('table_rdv'));
});
})();
</script>
<footer class="site-footer">
<div class="container">
<p><?= $translations['copyr'] ?? '&copy; '.date('Y') ?></p>
<p><?php echo $translations['copyr'] ?? '&copy; ' . date('Y') ?></p>
</div>
</footer>
</body>
</html>
</html>
+235 -157
View File
@@ -1,52 +1,78 @@
<?php
// /public/cabinet/disponibilites_date.php (VERSION SECRÉTAIRE)
require_once __DIR__ . '/../includes/init_secretaire.php';
declare (strict_types = 1);
// /public/cabinet/disponibilites_date.php
require_once __DIR__ . '/../includes/init_secretaire.php';
if (!function_exists('h')) {
function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
}
if (! function_exists('h')) {
function h($s): string
{
return htmlspecialchars((string) $s, ENT_QUOTES, 'UTF-8');
}
}
// --- Contexte praticien obligatoire
$compteId = (int)($_SESSION['compte_id'] ?? 0);
$userRole = strtolower((string)($_SESSION['role'] ?? ''));
$praticien_id = isset($_GET['praticien_id']) ? (int)$_GET['praticien_id'] : 0;
// --- Contexte praticien obligatoire
$compteId = (int) ($_SESSION['compte_id'] ?? 0);
$userRole = strtolower((string) ($_SESSION['role'] ?? ''));
$praticien_id = (int) ($_GET['praticien_id'] ?? 0);
if ($praticien_id <= 0 || $userRole !== 'secretaire') { http_response_code(403); exit('Forbidden'); }
if ($praticien_id <= 0 || $userRole !== 'secretaire') {
http_response_code(403);
exit('Forbidden');
}
// --- Vérifier pivot + droit can_edit_slots
$st = $pdo->prepare("
SELECT can_edit_slots
// --- Vérifier pivot + droit can_edit_slots
$st = $pdo->prepare("
SELECT can_edit_slots
FROM secretaire_praticien
WHERE secretaire_user_id = :u
AND praticien_id = :p
AND is_active = 1
LIMIT 1
WHERE secretaire_user_id = :u
AND praticien_id = :p
AND is_active = 1
LIMIT 1
");
$st->execute(['u'=>$compteId,'p'=>$praticien_id]);
$pivot = $st->fetch(PDO::FETCH_ASSOC);
if (!$pivot || (int)$pivot['can_edit_slots'] !== 1) { http_response_code(403); exit('Forbidden'); }
$st->execute([
'u' => $compteId,
'p' => $praticien_id,
]);
$pivot = $st->fetch(PDO::FETCH_ASSOC);
// --- Optionnel: récupérer nom praticien pour titre
$st = $pdo->prepare("SELECT nom, prenom FROM praticien WHERE id = ? LIMIT 1");
$st->execute([$praticien_id]);
$prat = $st->fetch(PDO::FETCH_ASSOC);
$currentPraticienName = trim(($prat['prenom'] ?? '').' '.($prat['nom'] ?? ''));
if (! $pivot || (int) $pivot['can_edit_slots'] !== 1) {
http_response_code(403);
exit('Forbidden');
}
$erreur = null;
// --- Nom praticien
$st = $pdo->prepare("SELECT nom, prenom FROM praticien WHERE id = ? LIMIT 1");
$st->execute([$praticien_id]);
$prat = $st->fetch(PDO::FETCH_ASSOC);
// ---- Ajouter une plage datée (POST)
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['date_dispo'], $_POST['heure_debut'], $_POST['heure_fin'])) {
$date = trim((string)$_POST['date_dispo']); // YYYY-MM-DD
$debut = trim((string)$_POST['heure_debut']); // HH:MM
$fin = trim((string)$_POST['heure_fin']); // HH:MM
if (! $prat) {
http_response_code(404);
exit('Praticien introuvable');
}
$tz = new DateTimeZone('Europe/Paris');
$currentPraticienName = trim(((string) ($prat['prenom'] ?? '')) . ' ' . ((string) ($prat['nom'] ?? '')));
$erreur = null;
// ---- Ajouter une plage datée (POST + CSRF)
if (
$_SERVER['REQUEST_METHOD'] === 'POST'
&& ($_POST['action'] ?? '') === 'add'
&& isset($_POST['date_dispo'], $_POST['heure_debut'], $_POST['heure_fin'])
) {
require_post_csrf();
$date = trim((string) $_POST['date_dispo']); // YYYY-MM-DD
$debut = trim((string) $_POST['heure_debut']); // HH:MM
$fin = trim((string) $_POST['heure_fin']); // HH:MM
$tz = new DateTimeZone('Europe/Paris');
$today = (new DateTimeImmutable('now', $tz))->format('Y-m-d');
if (!preg_match('#^\d{4}-\d{2}-\d{2}$#', $date)) {
if (! preg_match('#^\d{4}-\d{2}-\d{2}$#', $date)) {
$erreur = $translations['erreur_format_date'] ?? "Format de date invalide.";
} elseif (!preg_match('#^\d{2}:\d{2}$#', $debut) || !preg_match('#^\d{2}:\d{2}$#', $fin)) {
} elseif (! preg_match('#^\d{2}:\d{2}$#', $debut) || ! preg_match('#^\d{2}:\d{2}$#', $fin)) {
$erreur = $translations['erreur_format_heure'] ?? "Format d’heure invalide.";
} elseif ($date < $today) {
$erreur = $translations['erreur_date_passee'] ?? "Date dans le passé.";
@@ -54,175 +80,222 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['date_dispo'], $_POST[
$erreur = $translations['erreur_debut_avant_fin'] ?? "Heure de début après l’heure de fin.";
}
if (!$erreur) {
if (! $erreur) {
try {
$stmt = $pdo->prepare("
INSERT INTO disponibilite_date (praticien_id, date_dispo, heure_debut, heure_fin)
VALUES (:p, :d, :hd, :hf)
INSERT INTO disponibilite_date (praticien_id, date_dispo, heure_debut, heure_fin)
VALUES (:p, :d, :hd, :hf)
");
$stmt->execute([
'p' => $praticien_id,
'd' => $date,
'hd' => $debut . ':00',
'hf' => $fin . ':00',
'p' => $praticien_id,
'd' => $date,
'hd' => $debut . ':00',
'hf' => $fin . ':00',
]);
header('Location: /public/cabinet/disponibilites_date.php?praticien_id='.$praticien_id.'&ok=added', true, 303);
header('Location: /public/cabinet/disponibilites_date.php?praticien_id=' . $praticien_id . '&ok=added', true, 303);
exit;
} catch (PDOException $e) {
$erreur = $translations['erreur_enregistrement'] ?? "Impossible d’enregistrer (doublon ou erreur).";
}
}
}
// ---- Supprimer une plage datée (GET)
if (isset($_GET['supprimer'])) {
$id = (int)$_GET['supprimer'];
// Vérifier appartenance
$own = $pdo->prepare("SELECT 1 FROM disponibilite_date WHERE id = :id AND praticien_id = :p LIMIT 1");
$own->execute(['id'=>$id,'p'=>$praticien_id]);
if ($own->fetchColumn()) {
$stmt = $pdo->prepare("DELETE FROM disponibilite_date WHERE id = :id AND praticien_id = :p");
$stmt->execute(['id'=>$id,'p'=>$praticien_id]);
header('Location: /public/cabinet/disponibilites_date.php?praticien_id='.$praticien_id.'&ok=deleted', true, 303);
exit;
} else {
http_response_code(403); exit('Forbidden');
}
}
// ---- Liste
$stmt = $pdo->prepare("
SELECT id, date_dispo, heure_debut, heure_fin
// ---- Liste
$stmt = $pdo->prepare("
SELECT id, date_dispo, heure_debut, heure_fin
FROM disponibilite_date
WHERE praticien_id = ?
ORDER BY date_dispo ASC, heure_debut ASC
WHERE praticien_id = ?
ORDER BY date_dispo ASC, heure_debut ASC
");
$stmt->execute([$praticien_id]);
$dates = $stmt->fetchAll(PDO::FETCH_ASSOC);
$stmt->execute([$praticien_id]);
$dates = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Header cabinet
$PAGE_TITRE = $translations['titre_disponibilites_date'] ?? 'Disponibilités (dates)';
$HEAD_EXTRA = <<<HTML
<script src="/js/sweetalert2@11.js"></script>
$PAGE_TITLE = $translations['titre_disponibilites_date'] ?? 'Disponibilités (dates)';
$HEAD_EXTRA = <<<HTML
<script src="/js/sweetalert2@11.js"></script>
HTML;
require_once __DIR__ . '/../includes/header_cabinet.php';
require_once __DIR__ . '/../includes/header_cabinet.php';
?>
<div class="main1">
<div class="container py-5">
<div class="shadow p-4 rounded bg-light">
<h2 class="mb-4 text-center">
<?= h($currentPraticienName ?: ($translations['h2_disponibilites_date'] ?? 'Disponibilités ponctuelles')) ?>
<?php if ($currentPraticienName): ?>
- <?= h($translations['h2_disponibilites_date'] ?? 'Disponibilités ponctuelles') ?>
<?php endif; ?>
</h2>
<div class="d-flex justify-content-between align-items-center mb-3">
<a class="btn btn-outline-secondary"
href="/public/cabinet/disponibilites.php?praticien_id=<?= (int)$praticien_id ?>">
<?= h($translations['retour_au_planning'] ?? 'Retour') ?>
</a>
</div>
<div class="table-responsive mb-4">
<table class="table table-bordered table-striped align-middle text-center mobile-cards" id="table_dispo_date">
<thead class="table-light">
<tr>
<th><?= h($translations['cl_date_rdv'] ?? 'Date') ?></th>
<th><?= h($translations['table_debut'] ?? 'Début') ?></th>
<th><?= h($translations['table_fin'] ?? 'Fin') ?></th>
<th><?= h($translations['table_action'] ?? 'Action') ?></th>
</tr>
</thead>
<tbody>
<?php if ($dates): foreach ($dates as $d): ?>
<tr>
<td><?= h(date('d/m/Y', strtotime((string)$d['date_dispo']))) ?></td>
<td><?= h(substr((string)$d['heure_debut'], 0, 5)) ?></td>
<td><?= h(substr((string)$d['heure_fin'], 0, 5)) ?></td>
<td>
<button type="button" class="btn btn-sm btn-danger js-del" data-id="<?= (int)$d['id'] ?>">
<?= h($translations['btn_supprimer'] ?? 'Supprimer') ?>
</button>
</td>
</tr>
<?php endforeach; else: ?>
<tr><td colspan="4"><?= h($translations['aucun_creneau'] ?? 'Aucun créneau.') ?></td></tr>
<div class="container py-5">
<div class="shadow p-4 rounded bg-light">
<h2 class="mb-4 text-center">
<?php echo h($currentPraticienName ?: ($translations['h2_disponibilites_date'] ?? 'Disponibilités ponctuelles')) ?>
<?php if ($currentPraticienName): ?>
- <?php echo h($translations['h2_disponibilites_date'] ?? 'Disponibilités ponctuelles') ?>
<?php endif; ?>
</tbody>
</table>
</div>
</h2>
<div class="card shadow-sm p-4">
<h4 class="mb-3 text-center"><?= h($translations['card_ajouter_creneau_date'] ?? 'Ajouter une disponibilité ponctuelle') ?></h4>
<div class="d-flex justify-content-between align-items-center mb-3">
<a class="btn btn-outline-secondary"
href="/public/cabinet/disponibilites.php?praticien_id=<?php echo (int) $praticien_id ?>">
<?php echo h($translations['retour_au_planning'] ?? 'Retour') ?>
</a>
</div>
<form method="POST">
<input type="hidden" name="praticien_id" value="<?= (int)$praticien_id ?>">
<div class="mb-3">
<label class="form-label" for="date_dispo"><?= h($translations['cl_date_rdv'] ?? 'Date') ?></label>
<input type="date" name="date_dispo" id="date_dispo" class="form-control" required>
</div>
<div class="table-responsive mb-4">
<table class="table table-bordered table-striped align-middle text-center mobile-cards" id="table_dispo_date">
<thead class="table-light">
<tr>
<th><?php echo h($translations['cl_date_rdv'] ?? 'Date') ?></th>
<th><?php echo h($translations['table_debut'] ?? 'Début') ?></th>
<th><?php echo h($translations['table_fin'] ?? 'Fin') ?></th>
<th><?php echo h($translations['table_action'] ?? 'Action') ?></th>
</tr>
</thead>
<tbody>
<?php if ($dates): ?>
<?php foreach ($dates as $d): ?>
<tr>
<td><?php echo h(date('d/m/Y', strtotime((string) $d['date_dispo']))) ?></td>
<td><?php echo h(substr((string) $d['heure_debut'], 0, 5)) ?></td>
<td><?php echo h(substr((string) $d['heure_fin'], 0, 5)) ?></td>
<td>
<button type="button" class="btn btn-sm btn-danger js-del" data-id="<?php echo (int) $d['id'] ?>">
<?php echo h($translations['btn_supprimer'] ?? 'Supprimer') ?>
</button>
</td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="4"><?php echo h($translations['aucun_creneau'] ?? 'Aucun créneau.') ?></td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<div class="mb-3">
<label class="form-label" for="heure_debut"><?= h($translations['label_heure_debut'] ?? 'Heure de début') ?></label>
<input type="time" name="heure_debut" id="heure_debut" class="form-control" required>
</div>
<div class="card shadow-sm p-4">
<h4 class="mb-3 text-center"><?php echo h($translations['card_ajouter_creneau_date'] ?? 'Ajouter une disponibilité ponctuelle') ?></h4>
<div class="mb-3">
<label class="form-label" for="heure_fin"><?= h($translations['label_heure_fin'] ?? 'Heure de fin') ?></label>
<input type="time" name="heure_fin" id="heure_fin" class="form-control" required>
</div>
<form method="POST">
<input type="hidden" name="action" value="add">
<input type="hidden" name="csrf_token" value="<?php echo h(ensure_csrf_token()) ?>">
<button type="submit" class="btn btn-primary w-100">
<?= h($translations['btn_ajouter'] ?? 'Ajouter') ?>
</button>
</form>
<div class="mb-3">
<label class="form-label" for="date_dispo"><?php echo h($translations['cl_date_rdv'] ?? 'Date') ?></label>
<input type="date" name="date_dispo" id="date_dispo" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label" for="heure_debut"><?php echo h($translations['label_heure_debut'] ?? 'Heure de début') ?></label>
<input type="time" name="heure_debut" id="heure_debut" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label" for="heure_fin"><?php echo h($translations['label_heure_fin'] ?? 'Heure de fin') ?></label>
<input type="time" name="heure_fin" id="heure_fin" class="form-control" required>
</div>
<button type="submit" class="btn btn-primary w-100">
<?php echo h($translations['btn_ajouter'] ?? 'Ajouter') ?>
</button>
</form>
</div>
</div>
</div>
</div>
</div>
<script>
const csrfToken = <?php echo json_encode(ensure_csrf_token(), JSON_UNESCAPED_UNICODE) ?>;
const praticienId = <?php echo (int) $praticien_id ?>;
document.addEventListener('DOMContentLoaded', function(){
const okParam = '<?= isset($_GET['ok']) ? addslashes((string)$_GET['ok']) : '' ?>';
const erreurMsg = '<?= $erreur ? addslashes((string)$erreur) : '' ?>';
const okParam = <?php echo json_encode(isset($_GET['ok']) ? (string) $_GET['ok'] : '', JSON_UNESCAPED_UNICODE) ?>;
const erreurMsg = <?php echo json_encode($erreur ?: '', JSON_UNESCAPED_UNICODE) ?>;
if (okParam === 'added') {
Swal.fire({ icon:'success', title:'<?= addslashes($translations["msg_creneau_ajoute"] ?? "Créneau ajouté.") ?>', timer:1800, showConfirmButton:false });
} else if (okParam === 'deleted') {
Swal.fire({ icon:'success', title:'<?= addslashes($translations["msg_creneau_supprime"] ?? "Créneau supprimé.") ?>', timer:1800, showConfirmButton:false });
Swal.fire({
icon: 'success',
title: <?php echo json_encode($translations["msg_creneau_ajoute"] ?? "Créneau ajouté.", JSON_UNESCAPED_UNICODE) ?>,
timer: 1800,
showConfirmButton: false
});
if (window.history.replaceState) {
window.history.replaceState({}, document.title, window.location.pathname + '?praticien_id=' + encodeURIComponent(praticienId));
}
}
if (erreurMsg) {
Swal.fire({ icon:'error', title:'<?= addslashes($translations["erreur_titre"] ?? "Erreur") ?>', text:erreurMsg, confirmButtonText:'OK' });
Swal.fire({
icon: 'error',
title: <?php echo json_encode($translations["erreur_titre"] ?? "Erreur", JSON_UNESCAPED_UNICODE) ?>,
text: erreurMsg,
confirmButtonText: 'OK'
});
}
});
document.addEventListener('click', async function(e){
const btn = e.target.closest('.js-del');
if (!btn) return;
const id = btn.dataset.id;
if (!id) return;
const result = await Swal.fire({
icon: 'warning',
title: '<?= addslashes($translations["confirm_title"] ?? "Confirmation") ?>',
text: '<?= addslashes($translations["confirm_supprimer_creneau"] ?? "Supprimer ce créneau ?") ?>',
title: <?php echo json_encode($translations["confirm_title"] ?? "Confirmation", JSON_UNESCAPED_UNICODE) ?>,
text: <?php echo json_encode($translations["confirm_supprimer_creneau"] ?? "Supprimer ce créneau ?", JSON_UNESCAPED_UNICODE) ?>,
showCancelButton: true,
confirmButtonText: '<?= addslashes($translations["btn_da_udalit"] ?? "Oui") ?>',
cancelButtonText: '<?= addslashes($translations["btn_annuler"] ?? "Annuler") ?>',
confirmButtonText: <?php echo json_encode($translations["btn_da_udalit"] ?? "Oui", JSON_UNESCAPED_UNICODE) ?>,
cancelButtonText: <?php echo json_encode($translations["btn_annuler"] ?? "Annuler", JSON_UNESCAPED_UNICODE) ?>,
reverseButtons: true
});
if (result.isConfirmed) {
const pid = '<?= (int)$praticien_id ?>';
window.location.href = '?praticien_id=' + encodeURIComponent(pid) + '&supprimer=' + encodeURIComponent(id);
}
if (!result.isConfirmed) return;
const formData = new FormData();
formData.append('id', String(id));
formData.append('praticien_id', String(praticienId));
formData.append('csrf_token', csrfToken);
fetch('/public/cabinet/supprimer_disponibilite_date.php', {
method: 'POST',
headers: {
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: formData
})
.then(async r => {
const text = await r.text();
try {
return JSON.parse(text);
} catch (e) {
return { ok: false, message: text };
}
})
.then(data => {
if (data.ok) {
Swal.fire({
icon: 'success',
title: <?php echo json_encode($translations["msg_creneau_supprime"] ?? "Créneau supprimé.", JSON_UNESCAPED_UNICODE) ?>,
timer: 1400,
showConfirmButton: false
}).then(() => {
window.location.reload();
});
} else {
Swal.fire({
icon: 'error',
title: <?php echo json_encode($translations["erreur_titre"] ?? "Erreur", JSON_UNESCAPED_UNICODE) ?>,
text: data.message || 'Suppression impossible.'
});
}
})
.catch(() => {
Swal.fire({
icon: 'error',
title: 'Erreur réseau',
text: 'Veuillez réessayer.'
});
});
});
</script>
@@ -238,15 +311,20 @@ document.addEventListener('click', async function(e){
});
document.documentElement.classList.add('mc-ready');
}
function ready(fn){ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fn); else fn(); }
ready(function(){ initMobileCards(document.getElementById('table_dispo_date')); });
function ready(fn){
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fn);
else fn();
}
ready(function(){
initMobileCards(document.getElementById('table_dispo_date'));
});
})();
</script>
<footer class="site-footer">
<div class="container">
<p><?= $translations['copyr'] ?? '&copy; '.date('Y') ?></p>
<p><?php echo $translations['copyr'] ?? '&copy; ' . date('Y') ?></p>
</div>
</footer>
</body>
</html>
</html>
+106
View File
@@ -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
View File
@@ -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;
+82 -21
View File
@@ -1,27 +1,88 @@
<?php
declare (strict_types = 1);
require_once __DIR__ . '/../includes/init_client.php';
header('Content-Type: application/json; charset=utf-8');
$client_id = $_SESSION['reference_id'];
$praticien_id = intval($_POST['praticien_id'] ?? 0);
if ($praticien_id > 0) {
// Évite les doublons
$check = $pdo->prepare("SELECT * FROM praticien_client WHERE client_id = ? AND praticien_id = ?");
$check->execute([$client_id, $praticien_id]);
if (!$check->fetch()) {
$stmt = $pdo->prepare("INSERT INTO praticien_client (client_id, praticien_id) VALUES (?, ?)");
$stmt->execute([$client_id, $praticien_id]);
echo $translations['praticien_ajoute_favoris'];
} else {
echo $translations['praticien_deja_ajoute'];
}
} else {
echo $translations['praticien_id_invalide'];
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
echo json_encode([
'ok' => false,
'message' => 'Méthode non autorisée.',
], JSON_UNESCAPED_UNICODE);
exit;
}
require_post_csrf();
$client_id = (int) ($_SESSION['reference_id'] ?? 0);
$praticien_id = (int) ($_POST['praticien_id'] ?? 0);
if ($client_id <= 0) {
echo json_encode([
'ok' => false,
'message' => $translations['erreur_generique'] ?? 'Erreur.',
], JSON_UNESCAPED_UNICODE);
exit;
}
if ($praticien_id <= 0) {
echo json_encode([
'ok' => false,
'message' => $translations['praticien_id_invalide'] ?? 'ID praticien invalide.',
], JSON_UNESCAPED_UNICODE);
exit;
}
try {
// Vérifie que le praticien existe
$exists = $pdo->prepare("SELECT id FROM praticien WHERE id = ? LIMIT 1");
$exists->execute([$praticien_id]);
if (! $exists->fetchColumn()) {
echo json_encode([
'ok' => false,
'message' => $translations['praticien_id_invalide'] ?? 'ID praticien invalide.',
], JSON_UNESCAPED_UNICODE);
exit;
}
// Évite les doublons
$check = $pdo->prepare("
SELECT 1
FROM praticien_client
WHERE client_id = ? AND praticien_id = ?
LIMIT 1
");
$check->execute([$client_id, $praticien_id]);
if ($check->fetchColumn()) {
echo json_encode([
'ok' => true,
'already_exists' => true,
'message' => $translations['praticien_deja_ajoute'] ?? 'Déjà ajouté aux favoris.',
], JSON_UNESCAPED_UNICODE);
exit;
}
$stmt = $pdo->prepare("
INSERT INTO praticien_client (client_id, praticien_id)
VALUES (?, ?)
");
$stmt->execute([$client_id, $praticien_id]);
echo json_encode([
'ok' => true,
'already_exists' => false,
'message' => $translations['praticien_ajoute_favoris'] ?? 'Praticien ajouté aux favoris.',
], JSON_UNESCAPED_UNICODE);
exit;
} catch (Throwable $e) {
error_log('ajouter_favorit.php: ' . $e->getMessage());
echo json_encode([
'ok' => false,
'message' => $translations['erreur_generique'] ?? 'Erreur serveur.',
], JSON_UNESCAPED_UNICODE);
exit;
}
+347 -318
View File
@@ -1,310 +1,333 @@
<?php
require_once __DIR__ . '/../includes/init_client.php';
require_once __DIR__ . '/../../push/push_endpoint.php';
require_post_csrf();
// PHPMailer (config centralisée)
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
// --------- Réponse JSON utilitaire ----------
function json_out(bool $ok, string $msg, array $extra = []): never {
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['ok'=>$ok, 'message'=>$msg] + $extra);
exit;
function json_out(bool $ok, string $msg, array $extra = []): never
{
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['ok' => $ok, 'message' => $msg] + $extra);
exit;
}
// --------- Envoi e-mail via PHPMailer (avec ICS optionnel + fallback) ----------
function send_email_phpmailer(
string $toEmail,
string $subject,
string $textBody,
string $htmlBody,
?string $icsFilename = null,
?string $icsContent = null
string $toEmail,
string $subject,
string $textBody,
string $htmlBody,
?string $icsFilename = null,
?string $icsContent = null
): array {
global $MAILCFG;
global $MAILCFG;
$mail = new PHPMailer(true);
try {
// SMTP
$mail->isSMTP();
$mail->Host = $MAILCFG['smtp']['host'];
$mail->Port = $MAILCFG['smtp']['port'];
$mail->SMTPSecure = $MAILCFG['smtp']['secure'];
$mail->SMTPAuth = $MAILCFG['smtp']['auth'];
$mail->Username = $MAILCFG['smtp']['username'];
$mail->Password = $MAILCFG['smtp']['password'];
$mail->CharSet = 'UTF-8';
$mail->Encoding = 'base64';
$mail->XMailer = '';
// Debug ponctuel : ?debug_mail=1
if (!empty($_GET['debug_mail'])) {
$mail->SMTPDebug = 2;
$mail->Debugoutput = function($str,$lvl){ error_log("SMTP[$lvl] ".$str); };
}
if (!empty($MAILCFG['bounce'])) $mail->Sender = $MAILCFG['bounce'];
$from = $MAILCFG['from'] ?? ['no-reply@uslugi.fr','USLUGI.FR'];
$rpto = $MAILCFG['reply_to'] ?? ['contact@uslugi.fr','USLUGI.FR'];
$mail->setFrom($from[0], $from[1] ?? '');
if (!empty($rpto[0])) $mail->addReplyTo($rpto[0], $rpto[1] ?? '');
$mail->addAddress($toEmail);
if (!empty($MAILCFG['suppress_auto'])) {
$mail->addCustomHeader('Auto-Submitted', 'auto-generated');
$mail->addCustomHeader('X-Auto-Response-Suppress', 'All');
}
$mail->isHTML(true);
$mail->Subject = $subject;
$mail->Body = $htmlBody;
$mail->AltBody = $textBody;
if ($icsFilename && $icsContent !== null && empty($_GET['no_ics'])) {
$mail->addStringAttachment(
$icsContent,
$icsFilename,
'base64',
'text/calendar; method=CANCEL; charset=UTF-8'
);
}
$mail->send();
return [true, null];
} catch (Throwable $e) {
$err = 'PHPMailer error: '.$e->getMessage();
if (!empty($mail->ErrorInfo)) $err .= ' | ErrorInfo: '.$mail->ErrorInfo;
error_log($err);
// Fallback : réessaye SANS ICS
$mail = new PHPMailer(true);
try {
if ($icsFilename && $icsContent !== null && empty($_GET['no_ics'])) {
$mail->clearAttachments();
// SMTP
$mail->isSMTP();
$mail->Host = $MAILCFG['smtp']['host'];
$mail->Port = $MAILCFG['smtp']['port'];
$mail->SMTPSecure = $MAILCFG['smtp']['secure'];
$mail->SMTPAuth = $MAILCFG['smtp']['auth'];
$mail->Username = $MAILCFG['smtp']['username'];
$mail->Password = $MAILCFG['smtp']['password'];
$mail->CharSet = 'UTF-8';
$mail->Encoding = 'base64';
$mail->XMailer = '';
// Debug ponctuel : ?debug_mail=1
if (! empty($_GET['debug_mail'])) {
$mail->SMTPDebug = 2;
$mail->Debugoutput = function ($str, $lvl) {error_log("SMTP[$lvl] " . $str);};
}
if (! empty($MAILCFG['bounce'])) {
$mail->Sender = $MAILCFG['bounce'];
}
$from = $MAILCFG['from'] ?? ['no-reply@uslugi.fr', 'USLUGI.FR'];
$rpto = $MAILCFG['reply_to'] ?? ['contact@uslugi.fr', 'USLUGI.FR'];
$mail->setFrom($from[0], $from[1] ?? '');
if (! empty($rpto[0])) {
$mail->addReplyTo($rpto[0], $rpto[1] ?? '');
}
$mail->addAddress($toEmail);
if (! empty($MAILCFG['suppress_auto'])) {
$mail->addCustomHeader('Auto-Submitted', 'auto-generated');
$mail->addCustomHeader('X-Auto-Response-Suppress', 'All');
}
$mail->isHTML(true);
$mail->Subject = $subject;
$mail->Body = $htmlBody;
$mail->AltBody = $textBody;
if ($icsFilename && $icsContent !== null && empty($_GET['no_ics'])) {
$mail->addStringAttachment(
$icsContent,
$icsFilename,
'base64',
'text/calendar; method=CANCEL; charset=UTF-8'
);
}
$mail->send();
return [true, 'sent_without_ics'];
}
} catch (Throwable $e2) {
$err2 = 'PHPMailer fallback error: '.$e2->getMessage();
if (!empty($mail->ErrorInfo)) $err2 .= ' | ErrorInfo: '.$mail->ErrorInfo;
error_log($err2);
return [true, null];
} catch (Throwable $e) {
$err = 'PHPMailer error: ' . $e->getMessage();
if (! empty($mail->ErrorInfo)) {
$err .= ' | ErrorInfo: ' . $mail->ErrorInfo;
}
error_log($err);
// Fallback : réessaye SANS ICS
try {
if ($icsFilename && $icsContent !== null && empty($_GET['no_ics'])) {
$mail->clearAttachments();
$mail->send();
return [true, 'sent_without_ics'];
}
} catch (Throwable $e2) {
$err2 = 'PHPMailer fallback error: ' . $e2->getMessage();
if (! empty($mail->ErrorInfo)) {
$err2 .= ' | ErrorInfo: ' . $mail->ErrorInfo;
}
error_log($err2);
}
return [false, $err];
}
return [false, $err];
}
}
// --------- Helpers e-mail (HTML/Texte) ----------
function email_html_cancel(array $data, array $t): string {
$brand = '<img src="https://uslugi.fr/public/uploads/img/logo_mail.png" width="150" alt="USLUGI.FR">';
$brand_bas ='УСЛУГИ.ФР';
return '<!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>USLUGI.FR</title></head>
function email_html_cancel(array $data, array $t): string
{
$brand = '<img src="https://uslugi.fr/public/uploads/img/logo_mail.png" width="150" alt="USLUGI.FR">';
$brand_bas = 'УСЛУГИ.ФР';
return '<!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>USLUGI.FR</title></head>
<body style="margin:0;padding:0;background:#f4f6f8;font-family:Arial,Helvetica,sans-serif;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f4f6f8;padding:24px 0;">
<tr><td align="center">
<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="background:#ffffff;border-radius:8px;overflow:hidden;">
<tr><td style="background:#38378B;color:#ffffff;padding:20px 24px;font-size:18px;font-weight:600;">'.$brand.'</td></tr>
<tr><td style="padding:24px 24px 8px 24px;color:#111;font-size:16px;line-height:1.5;">'.nl2br(htmlspecialchars($data['hello_line'])).'</td></tr>
<tr><td style="padding:8px 24px;color:#111;font-size:16px;line-height:1.6;">'.nl2br(htmlspecialchars($data['intro'])).'</td></tr>
<tr><td style="background:#38378B;color:#ffffff;padding:20px 24px;font-size:18px;font-weight:600;">' . $brand . '</td></tr>
<tr><td style="padding:24px 24px 8px 24px;color:#111;font-size:16px;line-height:1.5;">' . nl2br(htmlspecialchars($data['hello_line'])) . '</td></tr>
<tr><td style="padding:8px 24px;color:#111;font-size:16px;line-height:1.6;">' . nl2br(htmlspecialchars($data['intro'])) . '</td></tr>
<tr><td style="padding:16px 24px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border:1px solid #e5e7eb;border-radius:8px;">
<tr><td style="padding:14px 16px;font-size:14px;color:#6b7280;border-bottom:1px solid #e5e7eb;">'.htmlspecialchars($data['who_label']).'</td><td style="padding:14px 16px;font-size:14px;color:#111;">'.htmlspecialchars($data['who_value']).'</td></tr>
<tr><td style="padding:14px 16px;font-size:14px;color:#6b7280;">'.htmlspecialchars($data['date_label']).'</td><td style="padding:14px 16px;font-size:14px;color:#111;">'.htmlspecialchars($data['when']).'</td></tr>
<tr><td style="padding:14px 16px;font-size:14px;color:#6b7280;">'.htmlspecialchars($data['statut_label']).'</td><td style="padding:14px 16px;font-size:14px;color:#111;">'.htmlspecialchars($data['statut_value']).'</td></tr>
<tr><td style="padding:14px 16px;font-size:14px;color:#6b7280;border-bottom:1px solid #e5e7eb;">' . htmlspecialchars($data['who_label']) . '</td><td style="padding:14px 16px;font-size:14px;color:#111;">' . htmlspecialchars($data['who_value']) . '</td></tr>
<tr><td style="padding:14px 16px;font-size:14px;color:#6b7280;">' . htmlspecialchars($data['date_label']) . '</td><td style="padding:14px 16px;font-size:14px;color:#111;">' . htmlspecialchars($data['when']) . '</td></tr>
<tr><td style="padding:14px 16px;font-size:14px;color:#6b7280;">' . htmlspecialchars($data['statut_label']) . '</td><td style="padding:14px 16px;font-size:14px;color:#111;">' . htmlspecialchars($data['statut_value']) . '</td></tr>
</table>
</td></tr>'.
(!empty($data['cta_url']) ? '<tr><td align="center" style="padding:8px 24px 24px 24px;"><a href="'.htmlspecialchars($data['cta_url']).'" target="_blank" style="display:inline-block;text-decoration:none;padding:12px 20px;border-radius:999px;background:#E03C3F;color:#fff;font-weight:600;">'.htmlspecialchars($data['cta_label']).'</a></td></tr>' : '').
'<tr><td style="padding:8px 24px 24px 24px;color:#111;font-size:16px;line-height:1.6;">'.nl2br(htmlspecialchars($data['footer'])).'</td></tr>
</td></tr>' .
(! empty($data['cta_url']) ? '<tr><td align="center" style="padding:8px 24px 24px 24px;"><a href="' . htmlspecialchars($data['cta_url']) . '" target="_blank" style="display:inline-block;text-decoration:none;padding:12px 20px;border-radius:999px;background:#E03C3F;color:#fff;font-weight:600;">' . htmlspecialchars($data['cta_label']) . '</a></td></tr>' : '') .
'<tr><td style="padding:8px 24px 24px 24px;color:#111;font-size:16px;line-height:1.6;">' . nl2br(htmlspecialchars($data['footer'])) . '</td></tr>
</table>
<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="margin-top:12px;"><tr><td style="text-align:center;color:#9ca3af;font-size:12px;padding:8px 0;">© '.date('Y').' '.$brand_bas.' — '.htmlspecialchars($t['merci_simple'] ?? 'Merci').'</td></tr></table>
<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="margin-top:12px;"><tr><td style="text-align:center;color:#9ca3af;font-size:12px;padding:8px 0;">© ' . date('Y') . ' ' . $brand_bas . ' — ' . htmlspecialchars($t['merci_simple'] ?? 'Merci') . '</td></tr></table>
</td></tr>
</table></body></html>';
}
function email_text_cancel(array $data, array $t): string {
$lines = [];
$lines[] = $data['hello_line']; $lines[]='';
$lines[] = $data['intro']; $lines[]='';
$lines[] = $data['who_label'].': '.$data['who_value'];
$lines[] = $data['date_label'].': '.$data['when'];
$lines[] = $data['statut_label'].': '.$data['statut_value'];
if (!empty($data['cta_url'])) { $lines[]=''; $lines[] = $data['cta_label'].': '.$data['cta_url']; }
$lines[]=''; $lines[] = $data['footer'];
return implode("\n", $lines);
function email_text_cancel(array $data, array $t): string
{
$lines = [];
$lines[] = $data['hello_line'];
$lines[] = '';
$lines[] = $data['intro'];
$lines[] = '';
$lines[] = $data['who_label'] . ': ' . $data['who_value'];
$lines[] = $data['date_label'] . ': ' . $data['when'];
$lines[] = $data['statut_label'] . ': ' . $data['statut_value'];
if (! empty($data['cta_url'])) {$lines[] = '';
$lines[] = $data['cta_label'] . ': ' . $data['cta_url'];}
$lines[] = '';
$lines[] = $data['footer'];
return implode("\n", $lines);
}
// --------- ICS CANCEL ----------
function ics_escape(string $s): string {
$s = str_replace("\\","\\\\",$s);
$s = str_replace(";","\;",$s);
$s = str_replace(",","\,",$s);
return preg_replace("/\r\n|\r|\n/","\\n",$s);
function ics_escape(string $s): string
{
$s = str_replace("\\", "\\\\", $s);
$s = str_replace(";", "\;", $s);
$s = str_replace(",", "\,", $s);
return preg_replace("/\r\n|\r|\n/", "\\n", $s);
}
function build_ics_cancel(
string $uid,
DateTime $start,
?DateTime $end,
string $timezone,
string $summary,
string $organizerEmail,
array $attendees // [['cn'=>..., 'email'=>...]]
string $uid,
DateTime $start,
?DateTime $end,
string $timezone,
string $summary,
string $organizerEmail,
array $attendees // [['cn'=>..., 'email'=>...]]
): string {
$lines=[];
$lines[]="BEGIN:VCALENDAR";
$lines[]="VERSION:2.0";
$lines[]="PRODID:-//УСЛУГИ.ФР//RDV//FR";
$lines[]="METHOD:CANCEL";
$lines[]="BEGIN:VEVENT";
$lines[]="UID:".$uid;
$lines[]="DTSTAMP:".gmdate('Ymd\THis\Z');
$lines[]="STATUS:CANCELLED";
$lines[]="SEQUENCE:2";
$lines[]="SUMMARY:".ics_escape($summary);
if ($start) $lines[]="DTSTART;TZID={$timezone}:".$start->format('Ymd\THis');
if ($end) $lines[]="DTEND;TZID={$timezone}:".$end->format('Ymd\THis');
$lines[]="ORGANIZER:mailto:".$organizerEmail;
foreach ($attendees as $a) {
if (empty($a['email'])) continue;
$cn = ics_escape($a['cn'] ?? '');
$lines[]="ATTENDEE;ROLE=REQ-PARTICIPANT;CN={$cn}:mailto:".$a['email'];
}
$lines[]="END:VEVENT";
$lines[]="END:VCALENDAR";
return implode("\r\n",$lines)."\r\n";
$lines = [];
$lines[] = "BEGIN:VCALENDAR";
$lines[] = "VERSION:2.0";
$lines[] = "PRODID:-//УСЛУГИ.ФР//RDV//FR";
$lines[] = "METHOD:CANCEL";
$lines[] = "BEGIN:VEVENT";
$lines[] = "UID:" . $uid;
$lines[] = "DTSTAMP:" . gmdate('Ymd\THis\Z');
$lines[] = "STATUS:CANCELLED";
$lines[] = "SEQUENCE:2";
$lines[] = "SUMMARY:" . ics_escape($summary);
if ($start) {
$lines[] = "DTSTART;TZID={$timezone}:" . $start->format('Ymd\THis');
}
if ($end) {
$lines[] = "DTEND;TZID={$timezone}:" . $end->format('Ymd\THis');
}
$lines[] = "ORGANIZER:mailto:" . $organizerEmail;
foreach ($attendees as $a) {
if (empty($a['email'])) {
continue;
}
$cn = ics_escape($a['cn'] ?? '');
$lines[] = "ATTENDEE;ROLE=REQ-PARTICIPANT;CN={$cn}:mailto:" . $a['email'];
}
$lines[] = "END:VEVENT";
$lines[] = "END:VCALENDAR";
return implode("\r\n", $lines) . "\r\n";
}
// =================== LOGIQUE ===================
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
json_out(false, 'Méthode invalide.');
json_out(false, 'Méthode invalide.');
}
$client_id = $_SESSION['reference_id'] ?? null;
$id = (int)($_POST['id'] ?? 0);
if (!$client_id || $id <= 0) {
json_out(false, $translations['erreur_generique'] ?? 'Erreur.');
$id = (int) ($_POST['id'] ?? 0);
if (! $client_id || $id <= 0) {
json_out(false, $translations['erreur_generique'] ?? 'Erreur.');
}
try {
// =========================
// SNAPSHOT AVANT ANNULATION
// =========================
$beforeStmt = $pdo->prepare("
// SNAPSHOT AVANT ANNULATION
// =========================
$beforeStmt = $pdo->prepare("
SELECT statut, date_rdv, duree_minutes, lieu, praticien_id, client_id
FROM rendez_vous
WHERE id = ? AND client_id = ?
LIMIT 1
");
$beforeStmt->execute([$id, $client_id]);
$beforeRow = $beforeStmt->fetch(PDO::FETCH_ASSOC);
$beforeStmt->execute([$id, $client_id]);
$beforeRow = $beforeStmt->fetch(PDO::FETCH_ASSOC);
if (!$beforeRow) {
json_out(false, $translations['erreur_generique'] ?? 'Rendez-vous introuvable.');
exit;
}
if (! $beforeRow) {
json_out(false, $translations['erreur_generique'] ?? 'Rendez-vous introuvable.');
exit;
}
// ✅ compte_id praticien + client (pour badge counters)
$praticienCompteId = 0;
$clientCompteId = 0;
$praticienCompteId = 0;
$clientCompteId = 0;
if (!empty($beforeRow['praticien_id'])) {
$st = $pdo->prepare("SELECT id FROM compte WHERE role='praticien' AND reference_id=? LIMIT 1");
$st->execute([(int)$beforeRow['praticien_id']]);
$praticienCompteId = (int)($st->fetchColumn() ?: 0);
}
if (!empty($beforeRow['client_id'])) {
$st = $pdo->prepare("SELECT id FROM compte WHERE role='client' AND reference_id=? LIMIT 1");
$st->execute([(int)$beforeRow['client_id']]);
$clientCompteId = (int)($st->fetchColumn() ?: 0);
}
if (! empty($beforeRow['praticien_id'])) {
$st = $pdo->prepare("SELECT id FROM compte WHERE role='praticien' AND reference_id=? LIMIT 1");
$st->execute([(int) $beforeRow['praticien_id']]);
$praticienCompteId = (int) ($st->fetchColumn() ?: 0);
}
if (! empty($beforeRow['client_id'])) {
$st = $pdo->prepare("SELECT id FROM compte WHERE role='client' AND reference_id=? LIMIT 1");
$st->execute([(int) $beforeRow['client_id']]);
$clientCompteId = (int) ($st->fetchColumn() ?: 0);
}
// 1) Annuler (contrainte client_id)
$stmt = $pdo->prepare("UPDATE rendez_vous SET statut = 'annulé' WHERE id = ? AND client_id = ?");
$stmt->execute([$id, $client_id]);
// 1) Annuler (contrainte client_id)
$stmt = $pdo->prepare("UPDATE rendez_vous SET statut = 'annulé' WHERE id = ? AND client_id = ?");
$stmt->execute([$id, $client_id]);
if ($stmt->rowCount() === 0) {
// Rien modifié -> soit déjà annulé, soit pas à ce client, soit ID invalide
json_out(false, $translations['aucune_modification'] ?? 'Aucune modification effectuée.');
}
if ($stmt->rowCount() === 0) {
// Rien modifié -> soit déjà annulé, soit pas à ce client, soit ID invalide
json_out(false, $translations['aucune_modification'] ?? 'Aucune modification effectuée.');
}
// =========================
// LOG RDV (rdv_actions_log) : cancel
// =========================
$beforeJson = json_encode([
'statut' => $beforeRow['statut'],
'date_rdv' => $beforeRow['date_rdv'],
'duree_minutes' => (int)$beforeRow['duree_minutes'],
'lieu' => $beforeRow['lieu'],
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
// =========================
// LOG RDV (rdv_actions_log) : cancel
// =========================
$beforeJson = json_encode([
'statut' => $beforeRow['statut'],
'date_rdv' => $beforeRow['date_rdv'],
'duree_minutes' => (int) $beforeRow['duree_minutes'],
'lieu' => $beforeRow['lieu'],
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$afterJson = json_encode([
'statut' => 'annulé',
'date_rdv' => $beforeRow['date_rdv'],
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$afterJson = json_encode([
'statut' => 'annulé',
'date_rdv' => $beforeRow['date_rdv'],
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$logStmt = $pdo->prepare("
$logStmt = $pdo->prepare("
INSERT INTO rdv_actions_log
(rdv_id, action, by_user_id, by_role, before_json, after_json, created_at)
VALUES
(?, 'cancel', ?, 'client', ?, ?, NOW())
");
$logStmt->execute([(int)$id, (int)$client_id, $beforeJson, $afterJson]);
// =========================
$logStmt->execute([(int) $id, (int) $client_id, $beforeJson, $afterJson]);
// =========================
// PUSH RDV — annulation (client)
// =========================
try {
// ✅ Badge RDV : +1 pour le praticien (cancel par client)
if ($praticienCompteId > 0) {
$pdo->prepare("
try {
// ✅ Badge RDV : +1 pour le praticien (cancel par client)
if ($praticienCompteId > 0) {
$pdo->prepare("
INSERT INTO push_badge_counters (compte_id, msg_unread, rdv_unread)
VALUES (:cid, 0, 1)
ON DUPLICATE KEY UPDATE
rdv_unread = LEAST(99, rdv_unread + 1),
updated_at = NOW()
")->execute([':cid' => $praticienCompteId]);
}
}
// 🔔 Praticien : RDV annulé par le client
push_rdv_to_praticien(
$pdo,
(int)$beforeRow['praticien_id'],
$translations['push_rdv_cancel_praticien_title'] ?? 'Запись отменена',
$translations['push_rdv_cancel_praticien_body'] ?? 'Клиент отменил запись.',
'/public/praticien/rendez_vous.php?id='.(int)$id,
[
'rdv_id' => (int)$id,
'action' => 'cancel',
'from_role' => 'client',
'client_id' => (int)$beforeRow['client_id'],
'praticien_id' => (int)$beforeRow['praticien_id'],
'date_rdv' => $beforeRow['date_rdv'],
]
);
// 🔔 Praticien : RDV annulé par le client
push_rdv_to_praticien(
$pdo,
(int) $beforeRow['praticien_id'],
$translations['push_rdv_cancel_praticien_title'] ?? 'Запись отменена',
$translations['push_rdv_cancel_praticien_body'] ?? 'Клиент отменил запись.',
'/public/praticien/rendez_vous.php?id=' . (int) $id,
[
'rdv_id' => (int) $id,
'action' => 'cancel',
'from_role' => 'client',
'client_id' => (int) $beforeRow['client_id'],
'praticien_id' => (int) $beforeRow['praticien_id'],
'date_rdv' => $beforeRow['date_rdv'],
]
);
// 🔔 Client : confirmation annulation
push_rdv_to_client(
$pdo,
(int)$beforeRow['client_id'],
$translations['push_rdv_cancel_client_title'] ?? 'Запись отменена',
$translations['push_rdv_cancel_client_body'] ?? 'Ваша запись была успешно отменена.',
'/public/client/mes_rendez_vous.php',
[
'rdv_id' => (int)$id,
'action' => 'cancel'
]
);
// 🔔 Client : confirmation annulation
push_rdv_to_client(
$pdo,
(int) $beforeRow['client_id'],
$translations['push_rdv_cancel_client_title'] ?? 'Запись отменена',
$translations['push_rdv_cancel_client_body'] ?? 'Ваша запись была успешно отменена.',
'/public/client/mes_rendez_vous.php',
[
'rdv_id' => (int) $id,
'action' => 'cancel',
]
);
} catch (Throwable $e) {
error_log('push rdv cancel (client) failed: '.$e->getMessage());
}
} catch (Throwable $e) {
error_log('push rdv cancel (client) failed: ' . $e->getMessage());
}
// 2) Infos RDV pour e-mails
$info = $pdo->prepare("
// 2) Infos RDV pour e-mails
$info = $pdo->prepare("
SELECT rv.date_rdv, rv.duree_minutes, rv.uuid_ical,
p.email AS praticien_email, p.nom AS praticien_nom, p.prenom AS praticien_prenom,
c.prenom AS client_prenom, c.nom AS client_nom, c.email AS client_email
@@ -313,109 +336,115 @@ try {
JOIN client c ON rv.client_id = c.id
WHERE rv.id = ? AND rv.client_id = ?
");
$info->execute([$id, $client_id]);
$r = $info->fetch(PDO::FETCH_ASSOC);
$info->execute([$id, $client_id]);
$r = $info->fetch(PDO::FETCH_ASSOC);
if (!$r) {
json_out(false, $translations['erreur_generique'] ?? 'Erreur.');
}
if (! $r) {
json_out(false, $translations['erreur_generique'] ?? 'Erreur.');
}
// 3) Prépare mails
$tz = new DateTimeZone('Europe/Paris');
$start = new DateTime($r['date_rdv'], $tz);
$end = (clone $start)->modify('+'.(int)($r['duree_minutes'] ?? 60).' minutes');
$when = $start->format('d/m/Y H:i');
// 3) Prépare mails
$tz = new DateTimeZone('Europe/Paris');
$start = new DateTime($r['date_rdv'], $tz);
$end = (clone $start)->modify('+' . (int) ($r['duree_minutes'] ?? 60) . ' minutes');
$when = $start->format('d/m/Y H:i');
$pratFull = $r['praticien_prenom'].' '.$r['praticien_nom'];
$cliFull = $r['client_prenom'].' '.$r['client_nom'];
$pratFull = $r['praticien_prenom'] . ' ' . $r['praticien_nom'];
$cliFull = $r['client_prenom'] . ' ' . $r['client_nom'];
$cta_url_prat = 'https://uslugi.fr/public/praticien/agenda.php';
$cta_url_cli = 'https://uslugi.fr/public/client/mes_rendez_vous.php';
$cta_label = $translations['consultez_espace_pour_details'] ?? 'Voir les détails';
$cta_url_prat = 'https://uslugi.fr/public/praticien/agenda.php';
$cta_url_cli = 'https://uslugi.fr/public/client/mes_rendez_vous.php';
$cta_label = $translations['consultez_espace_pour_details'] ?? 'Voir les détails';
$statut_label = $translations['statut_label'] ?? 'Statut';
$statut_value = $translations['statut_annule'] ?? 'Annulé';
$statut_label = $translations['statut_label'] ?? 'Statut';
$statut_value = $translations['statut_annule'] ?? 'Annulé';
// ---- Mail praticien
$subject_p = $translations['email_annulation_sujet'] ?? 'Rendez-vous annulé';
$hello_p = ($translations['email_bonjour_dr'] ?? 'Bonjour Dr').' '.$pratFull.',';
$intro_p = ($translations['email_rdv_du'] ?? 'Le rendez-vous du').' '.$when.' '.($translations['email_avec'] ?? 'avec').' '.$cliFull.' '.($translations['email_annule_par_client'] ?? 'a été annulé par le client.');
// ---- Mail praticien
$subject_p = $translations['email_annulation_sujet'] ?? 'Rendez-vous annulé';
$hello_p = ($translations['email_bonjour_dr'] ?? 'Bonjour Dr') . ' ' . $pratFull . ',';
$intro_p = ($translations['email_rdv_du'] ?? 'Le rendez-vous du') . ' ' . $when . ' ' . ($translations['email_avec'] ?? 'avec') . ' ' . $cliFull . ' ' . ($translations['email_annule_par_client'] ?? 'a été annulé par le client.');
$data_p = [
'hello_line' => $hello_p,
'intro' => $intro_p,
'who_label' => $translations['client_label'] ?? 'Client',
'who_value' => $cliFull,
'date_label' => $translations['date_label'] ?? 'Date',
'when' => $when,
'statut_label'=> $statut_label,
'statut_value'=> $statut_value,
'cta_url' => $cta_url_prat,
'cta_label' => $cta_label,
'footer' => $translations['merci_simple'] ?? 'Merci.',
];
$text_p = email_text_cancel($data_p, $translations);
$html_p = email_html_cancel($data_p, $translations);
$data_p = [
'hello_line' => $hello_p,
'intro' => $intro_p,
'who_label' => $translations['client_label'] ?? 'Client',
'who_value' => $cliFull,
'date_label' => $translations['date_label'] ?? 'Date',
'when' => $when,
'statut_label' => $statut_label,
'statut_value' => $statut_value,
'cta_url' => $cta_url_prat,
'cta_label' => $cta_label,
'footer' => $translations['merci_simple'] ?? 'Merci.',
];
$text_p = email_text_cancel($data_p, $translations);
$html_p = email_html_cancel($data_p, $translations);
$ics_p = null; $ics_name_p = null;
if (!empty($r['uuid_ical'])) {
$ics_name_p = 'rdv_cancel_'.$r['uuid_ical'].'_praticien.ics';
$ics_p = build_ics_cancel(
$r['uuid_ical'], $start, $end, 'Europe/Paris',
($translations['rdv_avec'] ?? 'Rendez-vous avec').' '.$cliFull,
'rdv@uslugi.fr',
[
['cn'=>$cliFull, 'email'=>$r['client_email']],
['cn'=>$pratFull, 'email'=>$r['praticien_email']],
]
);
}
[$ok_p, $err_p] = send_email_phpmailer($r['praticien_email'], $subject_p, $text_p, $html_p, $ics_name_p, $ics_p);
if (!$ok_p) error_log("Annulation RDV: mail praticien NON envoyé: ".$err_p);
$ics_p = null;
$ics_name_p = null;
if (! empty($r['uuid_ical'])) {
$ics_name_p = 'rdv_cancel_' . $r['uuid_ical'] . '_praticien.ics';
$ics_p = build_ics_cancel(
$r['uuid_ical'], $start, $end, 'Europe/Paris',
($translations['rdv_avec'] ?? 'Rendez-vous avec') . ' ' . $cliFull,
'rdv@uslugi.fr',
[
['cn' => $cliFull, 'email' => $r['client_email']],
['cn' => $pratFull, 'email' => $r['praticien_email']],
]
);
}
[$ok_p, $err_p] = send_email_phpmailer($r['praticien_email'], $subject_p, $text_p, $html_p, $ics_name_p, $ics_p);
if (! $ok_p) {
error_log("Annulation RDV: mail praticien NON envoyé: " . $err_p);
}
// ---- Mail client
$subject_c = $translations['email_annulation_client_sujet'] ?? 'Confirmation d’annulation de votre rendez-vous';
$hello_c = sprintf($translations['mail_valid_bonjour_nom'] ?? 'Bonjour %s', $cliFull);
$intro_c = ($translations['email_votre_rdv_du'] ?? 'Votre rendez-vous du').' '.$when.' '.($translations['email_avec'] ?? 'avec').' '.(($translations['titre_dr'] ?? 'Dr').' '.$pratFull).' '.($translations['email_a_ete_annule'] ?? 'a été annulé.');
// ---- Mail client
$subject_c = $translations['email_annulation_client_sujet'] ?? 'Confirmation d’annulation de votre rendez-vous';
$hello_c = sprintf($translations['mail_valid_bonjour_nom'] ?? 'Bonjour %s', $cliFull);
$intro_c = ($translations['email_votre_rdv_du'] ?? 'Votre rendez-vous du') . ' ' . $when . ' ' . ($translations['email_avec'] ?? 'avec') . ' ' . (($translations['titre_dr'] ?? 'Dr') . ' ' . $pratFull) . ' ' . ($translations['email_a_ete_annule'] ?? 'a été annulé.');
$who_label_cli = $translations['praticien_label'] ?? ($translations['titre_dr'] ?? 'Dr');
$who_value_cli = ($translations['titre_dr'] ?? 'Dr').' '.$pratFull;
$who_label_cli = $translations['praticien_label'] ?? ($translations['titre_dr'] ?? 'Dr');
$who_value_cli = ($translations['titre_dr'] ?? 'Dr') . ' ' . $pratFull;
$data_c = [
'hello_line' => $hello_c,
'intro' => $intro_c,
'who_label' => $who_label_cli,
'who_value' => $who_value_cli,
'date_label' => $translations['date_label'] ?? 'Date',
'when' => $when,
'statut_label'=> $statut_label,
'statut_value'=> $statut_value,
'cta_url' => $cta_url_cli,
'cta_label' => $cta_label,
'footer' => $translations['merci_simple'] ?? 'Merci.',
];
$text_c = email_text_cancel($data_c, $translations);
$html_c = email_html_cancel($data_c, $translations);
$data_c = [
'hello_line' => $hello_c,
'intro' => $intro_c,
'who_label' => $who_label_cli,
'who_value' => $who_value_cli,
'date_label' => $translations['date_label'] ?? 'Date',
'when' => $when,
'statut_label' => $statut_label,
'statut_value' => $statut_value,
'cta_url' => $cta_url_cli,
'cta_label' => $cta_label,
'footer' => $translations['merci_simple'] ?? 'Merci.',
];
$text_c = email_text_cancel($data_c, $translations);
$html_c = email_html_cancel($data_c, $translations);
$ics_c = null; $ics_name_c = null;
if (!empty($r['uuid_ical'])) {
$ics_name_c = 'rdv_cancel_'.$r['uuid_ical'].'_client.ics';
$ics_c = build_ics_cancel(
$r['uuid_ical'], $start, $end, 'Europe/Paris',
($translations['rdv_avec'] ?? 'Rendez-vous avec').' '.(($translations['titre_dr'] ?? 'Dr').' '.$pratFull),
'rdv@uslugi.fr',
[
['cn'=>$cliFull, 'email'=>$r['client_email']],
['cn'=>$pratFull, 'email'=>$r['praticien_email']],
]
);
}
[$ok_c, $err_c] = send_email_phpmailer($r['client_email'], $subject_c, $text_c, $html_c, $ics_name_c, $ics_c);
if (!$ok_c) error_log("Annulation RDV: mail client NON envoyé: ".$err_c);
$ics_c = null;
$ics_name_c = null;
if (! empty($r['uuid_ical'])) {
$ics_name_c = 'rdv_cancel_' . $r['uuid_ical'] . '_client.ics';
$ics_c = build_ics_cancel(
$r['uuid_ical'], $start, $end, 'Europe/Paris',
($translations['rdv_avec'] ?? 'Rendez-vous avec') . ' ' . (($translations['titre_dr'] ?? 'Dr') . ' ' . $pratFull),
'rdv@uslugi.fr',
[
['cn' => $cliFull, 'email' => $r['client_email']],
['cn' => $pratFull, 'email' => $r['praticien_email']],
]
);
}
[$ok_c, $err_c] = send_email_phpmailer($r['client_email'], $subject_c, $text_c, $html_c, $ics_name_c, $ics_c);
if (! $ok_c) {
error_log("Annulation RDV: mail client NON envoyé: " . $err_c);
}
json_out(true, $translations['rdv_annule_ok'] ?? 'Rendez-vous annulé.');
json_out(true, $translations['rdv_annule_ok'] ?? 'Rendez-vous annulé.');
} catch (Throwable $e) {
error_log('Annulation RDV error: '.$e->getMessage());
json_out(false, $translations['erreur_generique'] ?? 'Erreur serveur.');
error_log('Annulation RDV error: ' . $e->getMessage());
json_out(false, $translations['erreur_generique'] ?? 'Erreur serveur.');
}
+56 -39
View File
@@ -1,36 +1,40 @@
<?php
require_once __DIR__ . '/../includes/init_client.php';
require_once __DIR__ . '/../includes/init_client.php';
$success = $erreur = '';
$id = $_SESSION['compte_id'];
$stmt = $pdo->prepare("SELECT email, mot_de_passe FROM compte WHERE id = ?");
$stmt->execute([$id]);
$compte = $stmt->fetch();
$success = $erreur = '';
$id = $_SESSION['compte_id'];
if (! $compte) {
die($translations['compte_introuvable']);
}
$stmt = $pdo->prepare("SELECT email FROM compte WHERE id = ?");
$stmt->execute([$id]);
$compte = $stmt->fetch();
$email_actuel = $compte['email'];
if (!$compte) die($translations['compte_introuvable']);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
require_post_csrf();
$email_actuel = $compte['email'];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$nouvel_email = trim($_POST['email'] ?? '');
$mdp1 = $_POST['mot_de_passe'] ?? '';
$mdp2 = $_POST['confirm_mot_de_passe'] ?? '';
$mdpActuel = $_POST['mot_de_passe_actuel'] ?? '';
$mdp1 = $_POST['mot_de_passe'] ?? '';
$mdp2 = $_POST['confirm_mot_de_passe'] ?? '';
if (!filter_var($nouvel_email, FILTER_VALIDATE_EMAIL)) {
$erreur = $translations['erreur_email_invalide_format'];
if (! password_verify($mdpActuel, (string) ($compte['mot_de_passe'] ?? ''))) {
$erreur = $translations['erreur_mdp_actuel_incorrect'] ?? 'Mot de passe actuel incorrect.';
} elseif (! filter_var($nouvel_email, FILTER_VALIDATE_EMAIL)) {
$erreur = $translations['erreur_email_non_valide'];
} elseif ($mdp1 && ($mdp1 !== $mdp2 || strlen($mdp1) < 8)) {
$erreur = $translations['erreur_mdp_invalide_ou_non_confirme'];
$erreur = $translations['erreur_mdp_invalides'];
} else {
// Vérifie que l'email n’est pas déjà pris (sauf pour soi)
$stmt = $pdo->prepare("SELECT id FROM compte WHERE email = ? AND id != ?");
$stmt->execute([$nouvel_email, $id]);
if ($stmt->fetch()) {
$erreur = $translations['email_deja_utilise'];
$erreur = $translations['err_email_deja_utilise'];
} else {
// Mise à jour
if ($mdp1) {
$hash = password_hash($mdp1, PASSWORD_DEFAULT);
$stmt = $pdo->prepare("UPDATE compte SET email = ?, mot_de_passe = ? WHERE id = ?");
@@ -39,35 +43,48 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$stmt = $pdo->prepare("UPDATE compte SET email = ? WHERE id = ?");
$stmt->execute([$nouvel_email, $id]);
}
$success = $translations['maj_effectuee_succes'];
$success = $translations['success_compte_mis_a_jour'];
$email_actuel = $nouvel_email;
// garder la valeur en mémoire pour la suite de la page
$compte['mot_de_passe'] = $mdp1 ? $hash : $compte['mot_de_passe'];
}
}
}
require_once '../includes/header.php';
}
require_once '../includes/header.php';
?>
<div class="main1">
<div class="container py-5">
<?php if ($erreur): ?>
<div class="alert alert-danger"><?= $erreur ?></div>
<div class="alert alert-danger"><?php echo $erreur ?></div>
<?php endif; ?>
<?php if ($success): ?>
<div class="alert alert-success"><?= $success ?></div>
<div class="alert alert-success"><?php echo $success ?></div>
<?php endif; ?>
<form method="POST" class="shadow p-4 rounded bg-light">
<h2 class="text-center mb-4"> <?= $translations['mes_infos_connexion'] ?></h2>
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(ensure_csrf_token(), ENT_QUOTES, 'UTF-8') ?>">
<h2 class="text-center mb-4"> <?php echo $translations['mes_infos_connexion'] ?></h2>
<div class="mb-3">
<label class="form-label"><?= $translations['label_email'] ?></label>
<input type="email" name="email" value="<?= htmlspecialchars($email_actuel) ?>" required class="form-control">
<label class="form-label"><?php echo $translations['label_email'] ?></label>
<input type="email" name="email" value="<?php echo htmlspecialchars($email_actuel) ?>" required class="form-control">
</div>
<div class="mb-3">
<label class="form-label"><?php echo htmlspecialchars($translations['label_mdp_actuel'] ?? 'Mot de passe actuel') ?></label>
<div class="field-wrapper">
<input type="password" name="mot_de_passe_actuel" id="mot_de_passe_actuel" class="form-control"
placeholder="<?php echo htmlspecialchars($translations['placeholder_mdp_actuel'] ?? 'Entrez votre mot de passe actuel') ?>" required>
<span class="toggle-password" onclick="toggle('mot_de_passe_actuel')">👁️</span>
</div>
</div>
<div class="mb-3">
<label class="form-label"><?= $translations['label_nouveau_mdp_facultatif'] ?></label>
<label class="form-label"><?php echo $translations['label_nouveau_mdp_facultatif'] ?></label>
<div class="field-wrapper">
<input type="password" name="mot_de_passe" id="mot_de_passe" placeholder="<?= $translations['nouveau_mdp_placeholder'] ?>" class="form-control">
<input type="password" name="mot_de_passe" id="mot_de_passe" placeholder="<?php echo $translations['nouveau_mdp_placeholder'] ?>" class="form-control">
<span class="toggle-password" onclick="toggle('mot_de_passe')">👁️</span>
</div>
</div>
@@ -77,18 +94,18 @@ require_once '../includes/header.php';
<div class="strength-fill"></div>
<div class="strength-fill"></div>
</div>
<div id="niveau-securite" class="mb-3 text-muted"><?= $translations['securite_indetermine'] ?></div>
<div id="niveau-securite" class="mb-3 text-muted"><?php echo $translations['securite_indetermine'] ?></div>
<div class="mb-4">
<label class="form-label"><?= $translations['label_confirmer_mdp'] ?></label>
<label class="form-label"><?php echo $translations['label_confirmer_mdp'] ?></label>
<div class="field-wrapper">
<input type="password" name="confirm_mot_de_passe" id="confirm_mot_de_passe" placeholder="<?= $translations['confirm_mdp_placeholder'] ?>" class="form-control">
<input type="password" name="confirm_mot_de_passe" id="confirm_mot_de_passe" placeholder="<?php echo $translations['confirm_mdp_placeholder'] ?>" class="form-control">
<span class="toggle-password" onclick="toggle('confirm_mot_de_passe')">👁️</span>
</div>
</div>
<div class="text-center">
<button type="submit" class="btn btn-primary"> <?= $translations['btn_mettre_a_jour'] ?></button>
<button type="submit" class="btn btn-primary"> <?php echo $translations['btn_mettre_a_jour'] ?></button>
</div>
</form>
</div>
@@ -104,7 +121,7 @@ document.getElementById('mot_de_passe').addEventListener('input', function () {
const niveauText = document.getElementById("niveau-securite");
segments.forEach(s => s.style.backgroundColor = '#e0e0e0');
niveauText.textContent = <?= json_encode($translations['securite_indetermine']) ?>;
niveauText.textContent = <?php echo json_encode($translations['securite_indetermine']) ?>;
niveauText.className = 'text-muted';
const longOk = val.length >= 8;
@@ -120,16 +137,16 @@ document.getElementById('mot_de_passe').addEventListener('input', function () {
if (force === 1) {
segments[0].style.backgroundColor = 'red';
niveauText.textContent = <?= json_encode($translations['securite_faible']) ?>;
niveauText.textContent = <?php echo json_encode($translations['securite_faible']) ?>;
niveauText.className = 'text-danger';
} else if (force === 2) {
segments[0].style.backgroundColor = 'orange';
segments[1].style.backgroundColor = 'orange';
niveauText.textContent = <?= json_encode($translations['securite_moyen']) ?>;
niveauText.textContent = <?php echo json_encode($translations['securite_moyen']) ?>;
niveauText.className = 'text-warning';
} else if (force >= 3) {
segments.forEach(s => s.style.backgroundColor = 'green');
niveauText.textContent = <?= json_encode($translations['securite_fort']) ?>;
niveauText.textContent = <?php echo json_encode($translations['securite_fort']) ?>;
niveauText.className = 'text-success';
}
});
@@ -137,7 +154,7 @@ document.getElementById('mot_de_passe').addEventListener('input', function () {
</div>
<footer class="site-footer">
<div class="container">
<p><?= $translations['copyr'] ?></p>
<p><?php echo $translations['copyr'] ?></p>
</div>
</footer>
</body>
+159 -115
View File
@@ -1,60 +1,102 @@
<?php
require_once __DIR__ . '/../includes/init_client.php';
require_once __DIR__ . '/../includes/init_client.php';
$praticien_id = (int) ($_GET['id'] ?? 0);
if ($praticien_id <= 0) {echo "<div class='container py-5'>ID invalide.</div>";exit;}
$currentLang = $lang ?? 'ru';
if (! in_array($currentLang, ['ru', 'fr', 'en'], true)) {
$currentLang = 'ru';
}
$praticien_id = (int)($_GET['id'] ?? 0);
if ($praticien_id <= 0) { echo "<div class='container py-5'>ID invalide.</div>"; exit; }
function h($s)
{return htmlspecialchars((string) $s, ENT_QUOTES, 'UTF-8');}
function percent_from_5($note)
{$n = max(0, min(5, (float) $note));return (int) round($n / 5 * 100);}
function pseudo_from($prenom, $nom)
{
$p = trim((string) $prenom);
$n = trim((string) $nom);
$p = $p !== '' ? mb_convert_case($p, MB_CASE_TITLE, 'UTF-8') : 'Client';
$i = $n !== '' ? mb_strtoupper(mb_substr($nom, 0, 1, 'UTF-8'), 'UTF-8') : '';
return $i ? ($p . '.' . $i) : $p;
}
function video_thumb_info(?string $url): ?array
{
if (! $url || ! trim($url)) {
return null;
}
function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
function percent_from_5($note){ $n = max(0,min(5,(float)$note)); return (int)round($n/5*100); }
function pseudo_from($prenom,$nom){
$p = trim((string)$prenom); $n = trim((string)$nom);
$p = $p!=='' ? mb_convert_case($p, MB_CASE_TITLE, 'UTF-8') : 'Client';
$i = $n!=='' ? mb_strtoupper(mb_substr($nom,0,1,'UTF-8'),'UTF-8') : '';
return $i ? ($p.'.'.$i) : $p;
}
function video_thumb_info(?string $url): ?array {
if (!$url || !trim($url)) return null; $u = trim($url);
if (preg_match('~(?:youtube\.com/(?:watch\?v=|embed/|shorts/)|youtu\.be/)([A-Za-z0-9_-]{6,})~i',$u,$m)){
$id=$m[1]; return ['provider'=>'youtube','id'=>$id,'thumb'=>"https://i.ytimg.com/vi/$id/hqdefault.jpg"];
}
if (preg_match('~dailymotion\.com/video/([A-Za-z0-9]+)|dai\.ly/([A-Za-z0-9]+)~i',$u,$m)){
$id=$m[1] ?: $m[2]; return ['provider'=>'dailymotion','id'=>$id,'thumb'=>"https://www.dailymotion.com/thumbnail/video/$id"];
}
$path=parse_url($u,PHP_URL_PATH)??''; if(preg_match('~\.(mp4|webm|ogg)$~i',$path)){ return ['provider'=>'file','id'=>null,'thumb'=>null]; }
if (stripos($u,'vimeo.com')!==false){ return ['provider'=>'vimeo','id'=>null,'thumb'=>null]; }
return ['provider'=>'other','id'=>null,'thumb'=>null];
}
$u = trim($url);
if (preg_match('~(?:youtube\.com/(?:watch\?v=|embed/|shorts/)|youtu\.be/)([A-Za-z0-9_-]{6,})~i', $u, $m)) {
$id = $m[1];return ['provider' => 'youtube', 'id' => $id, 'thumb' => "https://i.ytimg.com/vi/$id/hqdefault.jpg"];
}
if (preg_match('~dailymotion\.com/video/([A-Za-z0-9]+)|dai\.ly/([A-Za-z0-9]+)~i', $u, $m)) {
$id = $m[1] ?: $m[2];return ['provider' => 'dailymotion', 'id' => $id, 'thumb' => "https://www.dailymotion.com/thumbnail/video/$id"];
}
$path = parse_url($u, PHP_URL_PATH) ?? '';if (preg_match('~\.(mp4|webm|ogg)$~i', $path)) {return ['provider' => 'file', 'id' => null, 'thumb' => null];}
if (stripos($u, 'vimeo.com') !== false) {return ['provider' => 'vimeo', 'id' => null, 'thumb' => null];}
return ['provider' => 'other', 'id' => null, 'thumb' => null];
}
// 1) Praticien
$st = $pdo->prepare("SELECT id, titre, nom, prenom, telephone, adresse, ville, code_postal, specialite, presentation, photo, video, disponibilite
FROM praticien WHERE id = ?");
$st->execute([$praticien_id]);
$p = $st->fetch(PDO::FETCH_ASSOC);
if (!$p) { echo "<div class='container py-5'>".$translations['praticien_introuvable']."</div>"; exit; }
// 1) Praticien
$st = $pdo->prepare("
SELECT
p.id,
p.titre,
p.nom,
p.prenom,
p.telephone,
p.adresse,
p.ville,
p.code_postal,
p.specialite,
p.presentation,
p.photo,
p.video,
p.disponibilite,
COALESCE(pt.nom_affiche, p.nom) AS nom_affiche,
COALESCE(pt.prenom_affiche, p.prenom) AS prenom_affiche,
COALESCE(pt.ville_affichee, p.ville) AS ville_affichee,
COALESCE(pt.titre, p.titre) AS titre_affiche,
COALESCE(pt.specialite, p.specialite) AS specialite_affichee,
COALESCE(pt.presentation, p.presentation) AS presentation_affichee,
COALESCE(pt.disponibilite, p.disponibilite) AS disponibilite_affichee
FROM praticien p
LEFT JOIN praticien_traduction pt
ON pt.praticien_id = p.id
AND pt.langue = :lang
WHERE p.id = :id
LIMIT 1
");
$st->execute([
':lang' => $currentLang,
':id' => $praticien_id,
]);
$p = $st->fetch(PDO::FETCH_ASSOC);
if (! $p) {echo "<div class='container py-5'>" . $translations['praticien_introuvable'] . "</div>";exit;}
$name = $name = trim($p['prenom'] . ' ' . $p['nom']);
$photo = !empty($p['photo']) ? "/public/uploads/".ltrim($p['photo'],'/') : "/public/uploads/img/user_placeholder.png";
$name = trim(($p['prenom_affiche'] ?? $p['prenom'] ?? '') . ' ' . ($p['nom_affiche'] ?? $p['nom'] ?? ''));
$photo = ! empty($p['photo']) ? "/public/uploads/" . ltrim($p['photo'], '/') : "/public/uploads/img/user_placeholder.png";
// 2) Vidéos additionnelles
$vs = $pdo->prepare("SELECT id, url, title, ordre FROM praticien_video WHERE praticien_id=? ORDER BY ordre ASC, id DESC");
$vs->execute([$praticien_id]);
$videos = $vs->fetchAll(PDO::FETCH_ASSOC);
// 2) Vidéos additionnelles
$vs = $pdo->prepare("SELECT id, url, title, ordre FROM praticien_video WHERE praticien_id=? ORDER BY ordre ASC, id DESC");
$vs->execute([$praticien_id]);
$videos = $vs->fetchAll(PDO::FETCH_ASSOC);
// 3) Avis: moyenne + total + dernier
$avg = $pdo->prepare("SELECT ROUND(AVG(note),1) AS m, COUNT(*) AS c FROM avis_praticien WHERE praticien_id=? AND statut='publie'");
$avg->execute([$praticien_id]);
$stats = $avg->fetch(PDO::FETCH_ASSOC) ?: ['m'=>null,'c'=>0];
// 3) Avis: moyenne + total + dernier
$avg = $pdo->prepare("SELECT ROUND(AVG(note),1) AS m, COUNT(*) AS c FROM avis_praticien WHERE praticien_id=? AND statut='publie'");
$avg->execute([$praticien_id]);
$stats = $avg->fetch(PDO::FETCH_ASSOC) ?: ['m' => null, 'c' => 0];
$last = $pdo->prepare("SELECT a.note,a.titre,a.commentaire,a.visit_date,a.created_at,c.prenom,c.nom
$last = $pdo->prepare("SELECT a.note,a.titre,a.commentaire,a.visit_date,a.created_at,c.prenom,c.nom
FROM avis_praticien a
LEFT JOIN client c ON c.id=a.client_id
WHERE a.praticien_id=? AND a.statut='publie'
ORDER BY a.created_at DESC LIMIT 1");
$last->execute([$praticien_id]);
$lrow = $last->fetch(PDO::FETCH_ASSOC);
require_once __DIR__.'/../includes/header.php'; // inclut lang + styles
$last->execute([$praticien_id]);
$lrow = $last->fetch(PDO::FETCH_ASSOC);
require_once __DIR__ . '/../includes/header.php'; // inclut lang + styles
?>
<div class="main1">
<div class="container py-4">
@@ -63,75 +105,75 @@ require_once __DIR__.'/../includes/header.php'; // inclut lang + styles
<!-- Cadre 1 : Infos -->
<section class="panel">
<div class="info-scroll">
<h2 class="pro-name"><?= h($name) ?></h2>
<?php if($p['specialite']): ?><div><strong><?= h($translations['dp_specialite']) ?>:</strong> <?= h($p['specialite']) ?></div><?php endif; ?>
<!--<?php if($p['telephone']): ?><div><strong><?= h($translations['dp_telephone']) ?>:</strong> <?= h($p['telephone']) ?></div><?php endif; ?>-->
<?php if($p['titre']): ?><div><strong><?= h($translations['tarif_consultation']) ?>:</strong> <?= h($p['titre']) ?></div><?php endif; ?>
<?php if($p['disponibilite']): ?><div><strong><?= h($translations['moyens_payement']) ?>:</strong> <?= h($p['disponibilite']) ?></div><?php endif; ?>
<?php if($p['adresse']): ?><div><strong><?= h($translations['dp_adresse']) ?>:</strong> <?= nl2br(h($p['adresse'])) ?></div><?php endif; ?>
<?php if($p['ville'] || $p['code_postal']): ?><div><strong><?= h($translations['dp_ville_cp']) ?>:</strong> <?= h(trim(($p['ville']??'').' '.($p['code_postal']??''))) ?></div><?php endif; ?>
<?php if($p['presentation']): ?><div class="mt-3"><strong><?= h($translations['dp_description']) ?>:</strong><br><?= nl2br(h($p['presentation'])) ?></div><?php endif; ?>
<h2 class="pro-name"><?php echo h($name) ?></h2>
<?php if ($p['specialite_affichee']): ?><div><strong><?php echo h($translations['dp_specialite']) ?>:</strong> <?php echo h($p['specialite_affichee']) ?></div><?php endif; ?>
<!--<?php if ($p['telephone']): ?><div><strong><?php echo h($translations['dp_telephone']) ?>:</strong> <?php echo h($p['telephone']) ?></div><?php endif; ?>-->
<?php if ($p['titre_affiche']): ?><div><strong><?php echo h($translations['tarif_consultation']) ?>:</strong> <?php echo h($p['titre_affiche']) ?></div><?php endif; ?>
<?php if ($p['disponibilite_affichee']): ?><div><strong><?php echo h($translations['moyens_payement']) ?>:</strong> <?php echo h($p['disponibilite_affichee']) ?></div><?php endif; ?>
<?php if ($p['adresse']): ?><div><strong><?php echo h($translations['dp_adresse']) ?>:</strong> <?php echo nl2br(h($p['adresse'])) ?></div><?php endif; ?>
<?php if ($p['ville_affichee'] || $p['code_postal']): ?><div><strong><?php echo h($translations['dp_ville_cp']) ?>:</strong> <?php echo h(trim(($p['ville_affichee'] ?? '') . ' ' . ($p['code_postal'] ?? ''))) ?></div><?php endif; ?>
<?php if ($p['presentation_affichee']): ?><div class="mt-3"><strong><?php echo h($translations['dp_description']) ?>:</strong><br><?php echo nl2br(h($p['presentation_affichee'])) ?></div><?php endif; ?>
</div>
</section>
<!-- Centre : grande photo -->
<section class="big-photo">
<img class="img-fluid w-100" src="<?= h($photo) ?>" alt="Photo">
<img class="img-fluid w-100" src="<?php echo h($photo) ?>" alt="Photo">
</section>
<!-- Cadre 2 : Vidéos -->
<?php if($p['video'] != ''){ ?>
<?php if ($p['video'] != '') {?>
<aside class="panel">
<h2 class="pro-name"><?= h($translations['dp_videos']) ?></h2>
<h2 class="pro-name"><?php echo h($translations['dp_videos']) ?></h2>
<div class="vlist">
<?php
$alt = h($translations['video_alt'] ?? 'Vidéo');
$printed = false;
$alt = h($translations['video_alt'] ?? 'Vidéo');
$printed = false;
// Petite fonction d'affichage d’une vignette
$printThumb = function($provider, $id, $thumb, $rawUrl) use ($alt){
if (in_array($provider, ['youtube','dailymotion']) && $id){
echo '<a href="#" class="vitem d-block pro-video"
// Petite fonction d'affichage d’une vignette
$printThumb = function ($provider, $id, $thumb, $rawUrl) use ($alt) {
if (in_array($provider, ['youtube', 'dailymotion']) && $id) {
echo '<a href="#" class="vitem d-block pro-video"
data-bs-toggle="modal" data-bs-target="#videoModal"
data-provider="'.h($provider).'" data-id="'.h($id).'">';
echo $thumb
? '<img src="'.h($thumb).'" alt="'.$alt.'" loading="lazy">'
: '<img src="/public/uploads/img/video_placeholder.jpg" alt="'.$alt.'" loading="lazy">';
echo '</a>';
} else {
$safe = h($rawUrl);
echo '<a class="vitem d-block" href="'.$safe.'" target="_blank" rel="noopener">';
echo $thumb
? '<img src="'.h($thumb).'" alt="'.$alt.'" loading="lazy">'
: '<img src="/public/uploads/img/video_placeholder.jpg" alt="'.$alt.'" loading="lazy">';
echo '</a>';
}
};
data-provider="' . h($provider) . '" data-id="' . h($id) . '">';
echo $thumb
? '<img src="' . h($thumb) . '" alt="' . $alt . '" loading="lazy">'
: '<img src="/public/uploads/img/video_placeholder.jpg" alt="' . $alt . '" loading="lazy">';
echo '</a>';
} else {
$safe = h($rawUrl);
echo '<a class="vitem d-block" href="' . $safe . '" target="_blank" rel="noopener">';
echo $thumb
? '<img src="' . h($thumb) . '" alt="' . $alt . '" loading="lazy">'
: '<img src="/public/uploads/img/video_placeholder.jpg" alt="' . $alt . '" loading="lazy">';
echo '</a>';
}
};
if (!empty($p['video'])) {
$vi = video_thumb_info($p['video']);
$thumb = $vi['thumb'] ?? null;
$prov = $vi['provider'] ?? null;
$vid = $vi['id'] ?? null;
$printThumb($prov, $vid, $thumb, $p['video']);
$printed = true;
}
foreach ($videos as $v) {
$vi = video_thumb_info($v['url']);
$thumb = $vi['thumb'] ?? null;
$prov = $vi['provider'] ?? null;
$vid = $vi['id'] ?? null;
$printThumb($prov, $vid, $thumb, $v['url']);
$printed = true;
}
if (!$printed) {
echo '<div class="vitem"><img src="/public/uploads/img/video_placeholder.jpg" alt="'.$alt.'"></div>';
//echo '<div class="text-muted small mt-1">'.h($translations['dp_aucune_video']).'</div>';
}
?>
if (! empty($p['video'])) {
$vi = video_thumb_info($p['video']);
$thumb = $vi['thumb'] ?? null;
$prov = $vi['provider'] ?? null;
$vid = $vi['id'] ?? null;
$printThumb($prov, $vid, $thumb, $p['video']);
$printed = true;
}
foreach ($videos as $v) {
$vi = video_thumb_info($v['url']);
$thumb = $vi['thumb'] ?? null;
$prov = $vi['provider'] ?? null;
$vid = $vi['id'] ?? null;
$printThumb($prov, $vid, $thumb, $v['url']);
$printed = true;
}
if (! $printed) {
echo '<div class="vitem"><img src="/public/uploads/img/video_placeholder.jpg" alt="' . $alt . '"></div>';
//echo '<div class="text-muted small mt-1">'.h($translations['dp_aucune_video']).'</div>';
}
?>
</div>
</aside>
<?php } ?>
<?php }?>
</div>
<!-- Ligne du bas : Note / Dernier avis / Bouton RDV -->
@@ -139,14 +181,16 @@ require_once __DIR__.'/../includes/header.php'; // inclut lang + styles
<!-- Cadre 3 : Note -->
<div class="card shadow-sm p-3">
<div class="d-flex align-items-center gap-3">
<?php $m=(float)($stats['m'] ?? 0); $c=(int)($stats['c'] ?? 0); $w=percent_from_5($m); ?>
<?php $m = (float) ($stats['m'] ?? 0);
$c = (int) ($stats['c'] ?? 0);
$w = percent_from_5($m); ?>
<div class="rating" aria-label="Note moyenne">
<div class="stars-base">★★★★★</div>
<div class="stars-fill" style="width:<?= $w ?>%">★★★★★</div>
<div class="stars-fill" style="width:<?php echo $w ?>%">★★★★★</div>
</div>
<div>
<div><strong><?= h($translations['avis_note_moy']) ?>:</strong> <?= $m ? number_format($m,1,',',' ') : '—' ?>/5</div>
<div class="text-muted">(<?= $c.' '.h($translations['avis_total']) ?>)</div>
<div><strong><?php echo h($translations['avis_note_moy']) ?>:</strong> <?php echo $m ? number_format($m, 1, ',', ' ') : '—' ?>/5</div>
<div class="text-muted">(<?php echo $c . ' ' . h($translations['avis_total']) ?>)</div>
</div>
</div>
</div>
@@ -154,31 +198,31 @@ require_once __DIR__.'/../includes/header.php'; // inclut lang + styles
<!-- Cadre 4 : Dernier avis -->
<div class="card shadow-sm p-3 avis-card">
<div class="d-flex justify-content-between align-items-start mb-2">
<strong><?= h($translations['avis_dernier']) ?></strong>
<a class="btn btn-link p-0" href="/public/client/avis.php?praticien_id=<?= (int)$praticien_id ?>">
<?= h($translations['avis_lire_tous']) ?>
<strong><?php echo h($translations['avis_dernier']) ?></strong>
<a class="btn btn-link p-0" href="/public/client/avis.php?praticien_id=<?php echo (int) $praticien_id ?>">
<?php echo h($translations['avis_lire_tous']) ?>
</a>
</div>
<?php if ($lrow): ?>
<?php $lw=percent_from_5((float)$lrow['note']); ?>
<?php $lw = percent_from_5((float) $lrow['note']); ?>
<div class="rating mb-1">
<div class="stars-base">★★★★★</div>
<div class="stars-fill" style="width:<?= $lw ?>%">★★★★★</div>
<div class="stars-fill" style="width:<?php echo $lw ?>%">★★★★★</div>
</div>
<div class="meta">
<?= h(pseudo_from($lrow['prenom'] ?? '', $lrow['nom'] ?? '')) ?>
<?php if(!empty($lrow['visit_date'])): ?> • <?= date('d/m/Y', strtotime($lrow['visit_date'])) ?><?php endif; ?>
<?php echo h(pseudo_from($lrow['prenom'] ?? '', $lrow['nom'] ?? '')) ?>
<?php if (! empty($lrow['visit_date'])): ?> • <?php echo date('d/m/Y', strtotime($lrow['visit_date'])) ?><?php endif; ?>
</div>
<div><?= nl2br(h(mb_strimwidth((string)$lrow['commentaire'],0,320,'…','UTF-8'))) ?></div>
<div><?php echo nl2br(h(mb_strimwidth((string) $lrow['commentaire'], 0, 320, '…', 'UTF-8'))) ?></div>
<?php else: ?>
<div class="text-muted"><?= h($translations['avis_aucun']) ?></div>
<div class="text-muted"><?php echo h($translations['avis_aucun']) ?></div>
<?php endif; ?>
</div>
<!-- Bouton RDV -->
<div class="text-end">
<a class="btn btn-danger btn-lg" href="/public/client/reserver_rdv.php?praticien_id=<?= (int)$praticien_id ?>">
<?= h($translations['dp_btn_rdv']) ?>
<a class="btn btn-danger btn-lg" href="/public/client/reserver_rdv.php?praticien_id=<?php echo (int) $praticien_id ?>">
<?php echo h($translations['dp_btn_rdv']) ?>
</a>
</div>
</div>
@@ -191,8 +235,8 @@ require_once __DIR__.'/../includes/header.php'; // inclut lang + styles
<div class="modal-dialog modal-dialog-centered modal-xl">
<div class="modal-content border-0 shadow-lg rounded-4">
<div class="modal-header border-0">
<h5 class="modal-title" id="videoModalLabel"><?= h($translations['video_titre'] ?? 'Vidéo') ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="<?= h($translations['fermer'] ?? 'Fermer') ?>"></button>
<h5 class="modal-title" id="videoModalLabel"><?php echo h($translations['video_titre'] ?? 'Vidéo') ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="<?php echo h($translations['fermer'] ?? 'Fermer') ?>"></button>
</div>
<div class="modal-body pt-0">
<div class="ratio ratio-16x9 rounded-3 overflow-hidden bg-black">
@@ -204,7 +248,7 @@ require_once __DIR__.'/../includes/header.php'; // inclut lang + styles
</div>
<div class="modal-footer border-0">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
<?= h($translations['fermer'] ?? 'Fermer') ?>
<?php echo h($translations['fermer'] ?? 'Fermer') ?>
</button>
</div>
</div>
@@ -241,7 +285,7 @@ require_once __DIR__.'/../includes/header.php'; // inclut lang + styles
<footer class="site-footer">
<div class="container">
<p><?= $translations['copyr'] ?></p>
<p><?php echo $translations['copyr'] ?></p>
</div>
</footer>
</body>
+303 -218
View File
@@ -1,218 +1,271 @@
<?php
require_once __DIR__ . '/../includes/init_client.php';
require_once __DIR__ . '/../includes/init_client.php';
// Support recherche ville en russe + latin
// Essaie d'abord /public/includes/, sinon /includes/ (selon ton projet)
$__paths = [
__DIR__ . '/../includes/search_helpers.php',
dirname(__DIR__, 2) . '/includes/search_helpers.php',
];
foreach ($__paths as $__p) { if (is_file($__p)) { require_once $__p; break; } }
unset($__paths, $__p);
// Support recherche ville en russe + latin
// Essaie d'abord /public/includes/, sinon /includes/ (selon ton projet)
$__paths = [
__DIR__ . '/../includes/search_helpers.php',
dirname(__DIR__, 2) . '/includes/search_helpers.php',
];
foreach ($__paths as $__p) {if (is_file($__p)) {require_once $__p;
break;}}
unset($__paths, $__p);
/* =========================
/* =========================
Paramètres / Recherche
========================= */
$perPage = 12;
$page = max(1, (int)($_GET['page'] ?? 1));
$qRaw = trim((string)($_GET['q'] ?? ''));
$wRaw = trim((string)($_GET['where'] ?? ''));
$q = mb_strtolower($qRaw);
$w = mb_strtolower($wRaw);
$isSearch = ($q !== '' || $w !== '');
$perPage = 12;
$page = max(1, (int) ($_GET['page'] ?? 1));
$qRaw = trim((string) ($_GET['q'] ?? ''));
$wRaw = trim((string) ($_GET['where'] ?? ''));
$q = mb_strtolower($qRaw);
$w = mb_strtolower($wRaw);
$isSearch = ($q !== '' || $w !== '');
$currentLang = $lang ?? 'ru';
if (! in_array($currentLang, ['ru', 'fr', 'en'], true)) {
$currentLang = 'ru';
}
/* =========================
/* =========================
Helpers
========================= */
function video_thumb_info(?string $url): ?array {
if (!$url || !trim($url)) return null;
$u = trim($url);
function video_thumb_info(?string $url): ?array
{
if (! $url || ! trim($url)) {
return null;
}
// YouTube
if (preg_match('~(?:youtube\\.com/(?:watch\\?v=|embed/|shorts/)|youtu\\.be/)([A-Za-z0-9_-]{6,})~i', $u, $m)) {
$id = $m[1];
return ['provider'=>'youtube','id'=>$id, 'thumb'=>"https://i.ytimg.com/vi/$id/hqdefault.jpg"];
}
// Dailymotion
if (preg_match('~dailymotion\\.com/video/([A-Za-z0-9]+)|dai\\.ly/([A-Za-z0-9]+)~i', $u, $m)) {
$id = $m[1] ?: $m[2];
return ['provider'=>'dailymotion','id'=>$id,'thumb'=>"https://www.dailymotion.com/thumbnail/video/$id"];
}
// Fichiers directs
$path = parse_url($u, PHP_URL_PATH) ?? '';
if (preg_match('~\\.(mp4|webm|ogg)$~i', $path)) return ['provider'=>'file','id'=>null,'thumb'=>null];
if (strpos(strtolower($u), 'vimeo.com') !== false) return ['provider'=>'vimeo','id'=>null,'thumb'=>null];
return ['provider'=>'other','id'=>null,'thumb'=>null];
}
$u = trim($url);
function fullName(array $p): string {
return trim(($p['prenom'] ?? '').' '.($p['nom'] ?? ''));
}
// YouTube
if (preg_match('~(?:youtube\\.com/(?:watch\\?v=|embed/|shorts/)|youtu\\.be/)([A-Za-z0-9_-]{6,})~i', $u, $m)) {
$id = $m[1];
return ['provider' => 'youtube', 'id' => $id, 'thumb' => "https://i.ytimg.com/vi/$id/hqdefault.jpg"];
}
// Dailymotion
if (preg_match('~dailymotion\\.com/video/([A-Za-z0-9]+)|dai\\.ly/([A-Za-z0-9]+)~i', $u, $m)) {
$id = $m[1] ?: $m[2];
return ['provider' => 'dailymotion', 'id' => $id, 'thumb' => "https://www.dailymotion.com/thumbnail/video/$id"];
}
// Fichiers directs
$path = parse_url($u, PHP_URL_PATH) ?? '';
if (preg_match('~\\.(mp4|webm|ogg)$~i', $path)) {
return ['provider' => 'file', 'id' => null, 'thumb' => null];
}
function excerpt_words(string $html, int $words = 60): string {
$plain = trim(preg_replace('/\\s+/', ' ', strip_tags($html)));
if ($plain === '') return '';
$parts = preg_split('/\\s+/', $plain);
if (count($parts) <= $words) return $plain;
return implode(' ', array_slice($parts, 0, $words)).'…';
}
if (strpos(strtolower($u), 'vimeo.com') !== false) {
return ['provider' => 'vimeo', 'id' => null, 'thumb' => null];
}
/* =========================
return ['provider' => 'other', 'id' => null, 'thumb' => null];
}
function fullName(array $p): string
{
return trim(($p['prenom_affiche'] ?? $p['prenom'] ?? '') . ' ' . ($p['nom_affiche'] ?? $p['nom'] ?? ''));
}
function excerpt_words(string $html, int $words = 60): string
{
$plain = trim(preg_replace('/\\s+/', ' ', strip_tags($html)));
if ($plain === '') {
return '';
}
$parts = preg_split('/\\s+/', $plain);
if (count($parts) <= $words) {
return $plain;
}
return implode(' ', array_slice($parts, 0, $words)) . '…';
}
/* =========================
Requêtes (COUNT + LIST)
========================= */
$whereSql = [];
$params = [];
$whereSql = [];
$params = [];
/*==========================BLOCK WHERE AVEC ABONNEMENT VALIDE ==========================*/
$whereSql[] = "(date_expiration IS NOT NULL AND date_expiration >= DATE_SUB(NOW(), INTERVAL 14 DAY))";
/*=======================================================================================*/
/*==========================BLOCK WHERE AVEC ABONNEMENT VALIDE ==========================*/
$whereSql[] = "(p.date_expiration IS NOT NULL AND p.date_expiration >= DATE_SUB(NOW(), INTERVAL 14 DAY))";
/*=======================================================================================*/
if ($q !== '') {
$words = preg_split('/\s+/', $q, -1, PREG_SPLIT_NO_EMPTY);
if ($q !== '') {
$words = preg_split('/\s+/', $q, -1, PREG_SPLIT_NO_EMPTY);
$and = [];
foreach ($words as $i => $word) {
$k1 = ":q_name_$i";
$k2 = ":q_spec_$i";
$and = [];
foreach ($words as $i => $word) {
$k1 = ":q_name_$i";
$k2 = ":q_spec_$i";
$and[] = "(
LOWER(CONCAT(COALESCE(prenom,''),' ',COALESCE(nom,''))) LIKE $k1
OR LOWER(COALESCE(specialite,'')) LIKE $k2
)";
$and[] = "(
LOWER(CONCAT(COALESCE(pt.prenom_affiche, p.prenom, ''),' ',COALESCE(pt.nom_affiche, p.nom, ''))) LIKE $k1
OR LOWER(COALESCE(pt.specialite, p.specialite, '')) LIKE $k2
)";
$params[$k1] = '%'.$word.'%';
$params[$k2] = '%'.$word.'%';
}
$params[$k1] = '%' . $word . '%';
$params[$k2] = '%' . $word . '%';
}
$whereSql[] = '(' . implode(' AND ', $and) . ')';
}
$whereSql[] = '(' . implode(' AND ', $and) . ')';
}
if ($w !== '') {
// Candidats latins à partir d'une saisie RU (alias + translittération)
$villeCands = city_query_candidates($w);
// Ajoute aussi la saisie brute (utile si déjà en latin)
$villeCands[] = mb_strtolower($w, 'UTF-8');
$villeCands = array_values(array_unique($villeCands));
if ($w !== '') {
// Candidats latins à partir d'une saisie RU (alias + translittération)
$villeCands = city_query_candidates($w);
// Ajoute aussi la saisie brute (utile si déjà en latin)
$villeCands[] = mb_strtolower($w, 'UTF-8');
$villeCands = array_values(array_unique($villeCands));
$ors = [];
$ors = [];
// Ville (LOWER(...)) : on cherche par préfixe
foreach ($villeCands as $i => $cand) {
$key = ":wv$i";
$ors[] = "LOWER(COALESCE(pt.ville_affichee, p.ville, '')) LIKE $key";
$params[$key] = $cand . '%'; // ← passe en '%'.$cand.'%' si tu veux "contient"
}
// Ville (LOWER(...)) : on cherche par préfixe
foreach ($villeCands as $i => $cand) {
$key = ":wv$i";
$ors[] = "LOWER(COALESCE(ville,'')) LIKE $key";
$params[$key] = $cand . '%'; // ← passe en '%'.$cand.'%' si tu veux "contient"
}
// Code postal : on garde un LIKE simple sur la saisie brute
$ors[] = "COALESCE(p.code_postal,'') LIKE :wcp";
$params[':wcp'] = '%' . $w . '%';
// Code postal : on garde un LIKE simple sur la saisie brute
$ors[] = "COALESCE(code_postal,'') LIKE :wcp";
$params[':wcp'] = '%' . $w . '%';
$whereSql[] = '(' . implode(' OR ', $ors) . ')';
}
$whereClause = $whereSql ? (' WHERE ' . implode(' AND ', $whereSql)) : '';
$whereSql[] = '(' . implode(' OR ', $ors) . ')';
}
$whereClause = $whereSql ? (' WHERE '.implode(' AND ', $whereSql)) : '';
/* Total pour pagination (utile seulement si !isSearch, mais on calcule une fois) */
$sqlCount = "SELECT COUNT(*) FROM praticien".$whereClause;
$stmt = $pdo->prepare($sqlCount);
$stmt->execute($params);
$total = (int)$stmt->fetchColumn();
$totalPages = max(1, (int)ceil($total / $perPage));
/* Liste (paginée si pas de recherche, sinon tout) */
$sqlList = "
SELECT id, titre, nom, prenom, photo, specialite, ville, code_postal, presentation, video
FROM praticien
/* Total pour pagination (utile seulement si !isSearch, mais on calcule une fois) */
$sqlCount = "
SELECT COUNT(*)
FROM praticien p
LEFT JOIN praticien_traduction pt
ON pt.praticien_id = p.id
AND pt.langue = :lang
$whereClause
ORDER BY specialite ASC, nom ASC
";
if (!$isSearch) {
$sqlList .= " LIMIT :limit OFFSET :offset";
}
$stmt = $pdo->prepare($sqlList);
foreach ($params as $k=>$v) $stmt->bindValue($k, $v, PDO::PARAM_STR);
if (!$isSearch) {
$stmt->bindValue(':limit', $perPage, PDO::PARAM_INT);
$stmt->bindValue(':offset', ($page-1)*$perPage, PDO::PARAM_INT);
}
$stmt->execute();
$praticiens = $stmt->fetchAll(PDO::FETCH_ASSOC);
/* =========================
$stmt = $pdo->prepare($sqlCount);
$paramsCount = $params;
$paramsCount[':lang'] = $currentLang;
$stmt->execute($paramsCount);
$total = (int) $stmt->fetchColumn();
$totalPages = max(1, (int) ceil($total / $perPage));
/* Liste (paginée si pas de recherche, sinon tout) */
$sqlList = "
SELECT
p.id,
p.titre,
p.nom,
p.prenom,
p.photo,
p.ville,
p.code_postal,
p.video,
COALESCE(pt.nom_affiche, p.nom) AS nom_affiche,
COALESCE(pt.prenom_affiche, p.prenom) AS prenom_affiche,
COALESCE(pt.specialite, p.specialite) AS specialite_affichee,
COALESCE(pt.ville_affichee, p.ville) AS ville_affichee,
COALESCE(pt.presentation, p.presentation) AS presentation_affichee
FROM praticien p
LEFT JOIN praticien_traduction pt
ON pt.praticien_id = p.id
AND pt.langue = :lang
$whereClause
ORDER BY COALESCE(pt.specialite, p.specialite) ASC, COALESCE(pt.nom_affiche, p.nom) ASC
";
if (! $isSearch) {
$sqlList .= " LIMIT :limit OFFSET :offset";
}
$stmt = $pdo->prepare($sqlList);
$params[':lang'] = $currentLang;
foreach ($params as $k => $v) {
$stmt->bindValue($k, $v, PDO::PARAM_STR);
}
if (! $isSearch) {
$stmt->bindValue(':limit', $perPage, PDO::PARAM_INT);
$stmt->bindValue(':offset', ($page - 1) * $perPage, PDO::PARAM_INT);
}
$stmt->execute();
$praticiens = $stmt->fetchAll(PDO::FETCH_ASSOC);
/* =========================
Rendu partiel AJAX (#results)
========================= */
function render_results(array $praticiens, array $translations, bool $isSearch, int $page, int $totalPages): void {
?>
function render_results(array $praticiens, array $translations, bool $isSearch, int $page, int $totalPages): void
{
?>
<?php if ($isSearch): ?>
<h1 class="results-title"><?= htmlspecialchars($translations['res_poisk'] ?? 'Résultats', ENT_QUOTES, 'UTF-8') ?></h1>
<h1 class="results-title"><?php echo htmlspecialchars($translations['res_poisk'] ?? 'Résultats', ENT_QUOTES, 'UTF-8') ?></h1>
<div id="resultCount" class="result-count">
<?php
$count = count($praticiens);
echo $count === 0
? htmlspecialchars($translations['ne_naideno'] ?? 'Aucun résultat', ENT_QUOTES, 'UTF-8')
: htmlspecialchars(($translations['naideno'] ?? 'Résultats')." : ".$count, ENT_QUOTES, 'UTF-8');
?>
$count = count($praticiens);
echo $count === 0
? htmlspecialchars($translations['ne_naideno'] ?? 'Aucun résultat', ENT_QUOTES, 'UTF-8')
: htmlspecialchars(($translations['naideno'] ?? 'Résultats') . " : " . $count, ENT_QUOTES, 'UTF-8');
?>
</div>
<?php endif; ?>
<div class="pro-list" id="list">
<?php if (empty($praticiens)): ?>
<?php else: ?>
<?php foreach ($praticiens as $p):
$name = fullName($p);
$spec = $p['specialite'] ?? '';
$ville = $p['ville'] ?? '';
$cp = $p['code_postal'] ?? '';
$present= excerpt_words((string)($p['presentation'] ?? ''), 35);
$vinfo = !empty($p['video']) ? video_thumb_info($p['video']) : null;
$thumb = $vinfo['thumb'] ?? null;
$altVid = htmlspecialchars($translations['video_alt'] ?? 'Vidéo', ENT_QUOTES, 'UTF-8');
$href = !empty($p['video']) ? htmlspecialchars($p['video'], ENT_QUOTES, 'UTF-8') : '';
?>
$name = fullName($p);
$spec = $p['specialite_affichee'] ?? $p['specialite'] ?? '';
$ville = $p['ville_affichee'] ?? $p['ville'] ?? '';
$cp = $p['code_postal'] ?? '';
$present = excerpt_words((string) ($p['presentation_affichee'] ?? $p['presentation'] ?? ''), 35);
$vinfo = ! empty($p['video']) ? video_thumb_info($p['video']) : null;
$thumb = $vinfo['thumb'] ?? null;
$altVid = htmlspecialchars($translations['video_alt'] ?? 'Vidéo', ENT_QUOTES, 'UTF-8');
$href = ! empty($p['video']) ? htmlspecialchars($p['video'], ENT_QUOTES, 'UTF-8') : '';
?>
<article class="pro-card praticien"
data-name="<?= htmlspecialchars(mb_strtolower($name), ENT_QUOTES, 'UTF-8') ?>"
data-spec="<?= htmlspecialchars(mb_strtolower($spec), ENT_QUOTES, 'UTF-8') ?>"
data-ville="<?= htmlspecialchars(mb_strtolower($ville), ENT_QUOTES, 'UTF-8') ?>"
data-cp="<?= htmlspecialchars(mb_strtolower($cp), ENT_QUOTES, 'UTF-8') ?>">
data-name="<?php echo htmlspecialchars(mb_strtolower($name), ENT_QUOTES, 'UTF-8') ?>"
data-spec="<?php echo htmlspecialchars(mb_strtolower($spec), ENT_QUOTES, 'UTF-8') ?>"
data-ville="<?php echo htmlspecialchars(mb_strtolower($ville), ENT_QUOTES, 'UTF-8') ?>"
data-cp="<?php echo htmlspecialchars(mb_strtolower($cp), ENT_QUOTES, 'UTF-8') ?>">
<!-- Photo -->
<div class="pro-photo">
<a href="/public/client/detail_praticien.php?id=<?= (int)$p['id'] ?>"><img
src="<?= !empty($p['photo']) ? '/public/uploads/'.htmlspecialchars($p['photo'], ENT_QUOTES, 'UTF-8') : '/public/uploads/img/user_placeholder.png' ?>"
alt="<?= htmlspecialchars($translations['alt_photo'] ?? 'Photo du praticien', ENT_QUOTES, 'UTF-8') ?>"
<a href="/public/client/detail_praticien.php?id=<?php echo (int) $p['id'] ?>"><img
src="<?php echo ! empty($p['photo']) ? '/public/uploads/' . htmlspecialchars($p['photo'], ENT_QUOTES, 'UTF-8') : '/public/uploads/img/user_placeholder.png' ?>"
alt="<?php echo htmlspecialchars($translations['alt_photo'] ?? 'Photo du praticien', ENT_QUOTES, 'UTF-8') ?>"
loading="lazy"></a>
</div>
<!-- Infos -->
<div class="pro-info">
<h2 class="pro-name"><a href="/public/client/detail_praticien.php?id=<?= (int)$p['id'] ?>"><?= htmlspecialchars($name, ENT_QUOTES, 'UTF-8') ?></a></h2>
<?php if ($spec): ?><div class="pro-spec"><?= htmlspecialchars($spec, ENT_QUOTES, 'UTF-8') ?></div><?php endif; ?>
<h2 class="pro-name"><a href="/public/client/detail_praticien.php?id=<?php echo (int) $p['id'] ?>"><?php echo htmlspecialchars($name, ENT_QUOTES, 'UTF-8') ?></a></h2>
<?php if ($spec): ?><div class="pro-spec"><?php echo htmlspecialchars($spec, ENT_QUOTES, 'UTF-8') ?></div><?php endif; ?>
<?php if ($present): ?>
<p class="pro-desc"><?= nl2br(htmlspecialchars($present, ENT_QUOTES, 'UTF-8')) ?></p>
<p class="pro-desc"><?php echo nl2br(htmlspecialchars($present, ENT_QUOTES, 'UTF-8')) ?></p>
<?php endif; ?>
<a href="#" class="favori_lien" data-id="<?= (int)$p['id'] ?>" data-loading="0">
<?= htmlspecialchars($translations['plus_favorits'] ?? 'Ajouter aux favoris', ENT_QUOTES, 'UTF-8') ?>
<a href="#" class="favori_lien" data-id="<?php echo (int) $p['id'] ?>" data-loading="0">
<?php echo htmlspecialchars($translations['plus_favorits'] ?? 'Ajouter aux favoris', ENT_QUOTES, 'UTF-8') ?>
</a>
<a href="/public/client/detail_praticien.php?id=<?= (int)$p['id'] ?>">
<?= htmlspecialchars($translations['plus_details'] ?? 'Détails', ENT_QUOTES, 'UTF-8') ?>
<a href="/public/client/detail_praticien.php?id=<?php echo (int) $p['id'] ?>">
<?php echo htmlspecialchars($translations['plus_details'] ?? 'Détails', ENT_QUOTES, 'UTF-8') ?>
</a>
</div>
<!-- Vidéo -->
<?php if(!empty($p['video'])): ?>
<?php if (! empty($p['video'])): ?>
<div class="pro-video-col">
<a class="pro-video"
href="#"
data-bs-toggle="modal"
data-bs-target="#videoModal"
data-yturl="<?= $href ?>"
aria-label="<?= $altVid ?>">
data-yturl="<?php echo $href ?>"
aria-label="<?php echo $altVid ?>">
<?php if ($thumb): ?>
<img src="<?= htmlspecialchars($thumb, ENT_QUOTES, 'UTF-8') ?>" alt="<?= $altVid ?>" loading="lazy">
<img src="<?php echo htmlspecialchars($thumb, ENT_QUOTES, 'UTF-8') ?>" alt="<?php echo $altVid ?>" loading="lazy">
<?php else: ?>
<img src="/public/uploads/img/video_placeholder.jpg" alt="<?= $altVid ?>" loading="lazy">
<img src="/public/uploads/img/video_placeholder.jpg" alt="<?php echo $altVid ?>" loading="lazy">
<?php endif; ?>
</a>
</div>
@@ -220,8 +273,8 @@ function render_results(array $praticiens, array $translations, bool $isSearch,
<!-- CTA -->
<div class="pro-cta-col">
<a class="pro-cta" href="/public/client/reserver_rdv.php?praticien_id=<?= (int)$p['id'] ?>">
<?= htmlspecialchars($translations['den'] ?? 'Prendre RDV', ENT_QUOTES, 'UTF-8') ?>
<a class="pro-cta" href="/public/client/reserver_rdv.php?praticien_id=<?php echo (int) $p['id'] ?>">
<?php echo htmlspecialchars($translations['den'] ?? 'Prendre RDV', ENT_QUOTES, 'UTF-8') ?>
</a>
</div>
</article>
@@ -229,40 +282,40 @@ function render_results(array $praticiens, array $translations, bool $isSearch,
<?php endif; ?>
</div>
<?php if (!$isSearch && $totalPages > 1): ?>
<?php if (! $isSearch && $totalPages > 1): ?>
<nav class="pager" aria-label="Pagination">
<a class="pager-arrow <?= $page<=1?'is-disabled':'' ?>"
href="?page=<?= max(1,$page-1) ?>" aria-label="Page précédente">&laquo;&laquo;</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">&laquo;&laquo;</a>
<?php
$start = max(1, $page-2);
$end = min($totalPages, $page+2);
for ($p=$start; $p<=$end; $p++):
?>
<a class="pager-page <?= $p===$page?'is-active':'' ?>" href="?page=<?= $p ?>"><?= $p ?></a>
$start = max(1, $page - 2);
$end = min($totalPages, $page + 2);
for ($p = $start; $p <= $end; $p++):
?>
<a class="pager-page <?php echo $p === $page ? 'is-active' : '' ?>" href="?page=<?php echo $p ?>"><?php echo $p ?></a>
<?php endfor; ?>
<a class="pager-arrow <?= $page>=$totalPages?'is-disabled':'' ?>"
href="?page=<?= min($totalPages,$page+1) ?>" aria-label="Page suivante">&raquo;&raquo;</a>
<a class="pager-arrow <?php echo $page >= $totalPages ? 'is-disabled' : '' ?>"
href="?page=<?php echo min($totalPages, $page + 1) ?>" aria-label="Page suivante">&raquo;&raquo;</a>
</nav>
<?php endif;
}
}
/* Si appel AJAX, ne renvoyer que le fragment #results */
if (isset($_GET['ajax']) && $_GET['ajax'] === '1') {
header('Content-Type: text/html; charset=utf-8');
ob_start();
render_results($praticiens, $translations, $isSearch, $page, $totalPages);
echo ob_get_clean();
exit;
}
require_once '../includes/header.php';
?>
/* Si appel AJAX, ne renvoyer que le fragment #results */
if (isset($_GET['ajax']) && $_GET['ajax'] === '1') {
header('Content-Type: text/html; charset=utf-8');
ob_start();
render_results($praticiens, $translations, $isSearch, $page, $totalPages);
echo ob_get_clean();
exit;
}
require_once '../includes/header.php';
?>
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title><?= htmlspecialchars($translations['menu_liste_specialistes'] ?? 'Liste des praticiens', ENT_QUOTES, 'UTF-8') ?></title>
<title><?php echo htmlspecialchars($translations['menu_liste_specialistes'] ?? 'Liste des praticiens', ENT_QUOTES, 'UTF-8') ?></title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Design -->
<link rel="stylesheet" href="../../includes/custom_style.css">
@@ -295,14 +348,14 @@ require_once '../includes/header.php';
<span class="search-icon" aria-hidden="true">🔎</span>
<div class="input-wrap">
<input id="q" type="text"
placeholder="<?= htmlspecialchars($translations['n_special'] ?? 'spécialité, nom…', ENT_QUOTES, 'UTF-8') ?>"
aria-label="spécialité, nom" value="<?= htmlspecialchars($qRaw, ENT_QUOTES, 'UTF-8') ?>">
placeholder="<?php echo htmlspecialchars($translations['n_special'] ?? 'spécialité, nom…', ENT_QUOTES, 'UTF-8') ?>"
aria-label="spécialité, nom" value="<?php echo htmlspecialchars($qRaw, ENT_QUOTES, 'UTF-8') ?>">
</div>
<div class="divider" aria-hidden="true"></div>
<div class="input-wrap">
<input id="where" type="text"
placeholder="<?= htmlspecialchars($translations['gde'] ?? 'ville ou code postal', ENT_QUOTES, 'UTF-8') ?>"
aria-label="ville ou code postal" value="<?= htmlspecialchars($wRaw, ENT_QUOTES, 'UTF-8') ?>">
placeholder="<?php echo htmlspecialchars($translations['gde'] ?? 'ville ou code postal', ENT_QUOTES, 'UTF-8') ?>"
aria-label="ville ou code postal" value="<?php echo htmlspecialchars($wRaw, ENT_QUOTES, 'UTF-8') ?>">
</div>
</div>
</section>
@@ -316,7 +369,7 @@ require_once '../includes/header.php';
<footer class="site-footer">
<div class="container">
<p><?= $translations['copyr'] ?></p>
<p><?php echo $translations['copyr'] ?></p>
</div>
</footer>
@@ -325,8 +378,8 @@ require_once '../includes/header.php';
<div class="modal-dialog modal-dialog-centered modal-xl">
<div class="modal-content border-0 shadow-lg rounded-4">
<div class="modal-header border-0">
<h5 class="modal-title" id="videoModalLabel"><?= htmlspecialchars($translations['video_titre'] ?? 'Vidéo') ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="<?= htmlspecialchars($translations['fermer'] ?? 'Fermer') ?>"></button>
<h5 class="modal-title" id="videoModalLabel"><?php echo htmlspecialchars($translations['video_titre'] ?? 'Vidéo') ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="<?php echo htmlspecialchars($translations['fermer'] ?? 'Fermer') ?>"></button>
</div>
<div class="modal-body pt-0">
<div class="ratio ratio-16x9 rounded-3 overflow-hidden">
@@ -340,7 +393,7 @@ require_once '../includes/header.php';
</div>
<div class="modal-footer border-0">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
<?= htmlspecialchars($translations['fermer'] ?? 'Fermer') ?>
<?php echo htmlspecialchars($translations['fermer'] ?? 'Fermer') ?>
</button>
</div>
</div>
@@ -348,6 +401,9 @@ require_once '../includes/header.php';
</div>
<!-- JS -->
<script>
const csrfToken = <?php echo json_encode(ensure_csrf_token(), JSON_UNESCAPED_UNICODE) ?>;
</script>
<script>
(function(){
// Convertit n'importe quelle URL YouTube en URL d'embed nocookie
@@ -421,40 +477,69 @@ require_once '../includes/header.php';
}, false);
// Favoris (SweetAlert2)
function ajouterFavori(el, praticienId) {
if (el.dataset.loading === '1') return;
el.dataset.loading = '1';
fetch('/public/client/ajouter_favori.php', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'X-Requested-With': 'XMLHttpRequest'
},
body: new URLSearchParams({ praticien_id: praticienId }).toString()
})
.then(async (r) => {
const text = await r.text();
if (!r.ok) throw new Error(text || 'Erreur réseau');
const deja = /déjà|already/i.test(text);
el.classList.add('ajoute'); el.setAttribute('aria-pressed', 'true'); el.title = text;
if (typeof Swal !== 'undefined') {
Swal.fire({
icon: deja ? 'info' : 'success',
title: <?= json_encode($translations['sw_favori_ajoute_title'] ?? 'Favori ajouté !', JSON_UNESCAPED_UNICODE) ?>,
text: text,
timer: 1800,
showConfirmButton: false
});
} else { alert(text); }
})
.catch((err) => {
if (typeof Swal !== 'undefined') {
Swal.fire({ icon:'error', title:'Oups', text: err.message || 'Erreur.' });
} else { console.error(err); alert('Erreur.'); }
})
.finally(() => { el.dataset.loading = '0'; });
function ajouterFavori(el, praticienId) {
if (el.dataset.loading === '1') return;
el.dataset.loading = '1';
const formData = new FormData();
formData.append('praticien_id', String(praticienId));
formData.append('csrf_token', csrfToken);
fetch('/public/client/ajouter_favori.php', {
method: 'POST',
headers: {
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: formData
})
.then(async (r) => {
const text = await r.text();
let data;
try {
data = JSON.parse(text);
} catch (e) {
throw new Error(text || 'Erreur réseau');
}
if (!r.ok || !data.ok) {
throw new Error(data.message || 'Erreur.');
}
el.classList.add('ajoute');
el.setAttribute('aria-pressed', 'true');
el.title = data.message || '';
if (typeof Swal !== 'undefined') {
Swal.fire({
icon: data.already_exists ? 'info' : 'success',
title: <?php echo json_encode($translations['sw_favori_ajoute_title'] ?? 'Favori ajouté !', JSON_UNESCAPED_UNICODE) ?>,
text: data.message || '',
timer: 1800,
showConfirmButton: false
});
} else {
alert(data.message || 'OK');
}
})
.catch((err) => {
if (typeof Swal !== 'undefined') {
Swal.fire({
icon: 'error',
title: 'Oups',
text: err.message || 'Erreur.'
});
} else {
console.error(err);
alert('Erreur.');
}
})
.finally(() => {
el.dataset.loading = '0';
});
}
document.addEventListener('click', (e)=>{
const a = e.target.closest('.favori_lien');
if (!a) return;
@@ -462,10 +547,10 @@ require_once '../includes/header.php';
const id = a.dataset.id;
const T = {
title: <?= json_encode($translations['confirmation'] ?? 'Confirmation') ?>,
text: <?= json_encode($translations['voulez_vous_ajouter_favori'] ?? 'Ajouter ce praticien à vos favoris ?') ?>,
ok: <?= json_encode($translations['btn_oui'] ?? 'Oui') ?>,
cancel:<?= json_encode($translations['btn_annuler'] ?? 'Annuler') ?>
title: <?php echo json_encode($translations['confirmation'] ?? 'Confirmation') ?>,
text: <?php echo json_encode($translations['voulez_vous_ajouter_favori'] ?? 'Ajouter ce praticien à vos favoris ?') ?>,
ok: <?php echo json_encode($translations['btn_oui'] ?? 'Oui') ?>,
cancel:<?php echo json_encode($translations['btn_annuler'] ?? 'Annuler') ?>
};
if (typeof Swal !== 'undefined') {
+85 -52
View File
@@ -1,67 +1,68 @@
<?php
require_once __DIR__ . '/../includes/init_client.php';
require_once __DIR__ . '/../includes/init_client.php';
$client_id = $_SESSION['reference_id'];
$client_id = $_SESSION['reference_id'];
$stmt = $pdo->prepare("
$stmt = $pdo->prepare("
SELECT p.*
FROM praticien_client pc
JOIN praticien p ON pc.praticien_id = p.id
WHERE pc.client_id = ?
ORDER BY p.nom ASC
");
$stmt->execute([$client_id]);
$favoris = $stmt->fetchAll();
$stmt->execute([$client_id]);
$favoris = $stmt->fetchAll();
// util pour couper les mots de la description texte
function h($s)
{return htmlspecialchars((string) $s, ENT_QUOTES, 'UTF-8');}
function excerpt($text, $len = 160)
{
$t = trim(strip_tags((string) $text));
if (mb_strlen($t, 'UTF-8') <= $len) {
return $t;
}
// util pour couper les mots de la description texte
function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
function excerpt($text, $len=160){
$t = trim(strip_tags((string)$text));
if (mb_strlen($t,'UTF-8') <= $len) return $t;
return mb_substr($t, 0, $len, 'UTF-8') . '…';
}
}
$PAGE_TITLE = $translations['mes_praticiens_favoris_title'] ?? 'Mes praticiens favoris';
$HEAD_EXTRA = <<<HTML
$PAGE_TITLE = $translations['mes_praticiens_favoris_title'] ?? 'Mes praticiens favoris';
$HEAD_EXTRA = <<<HTML
<script src="/js/sweetalert2@11.js"></script>
HTML;
require_once '../includes/header.php';
require_once '../includes/header.php';
?>
<div class="main1">
<div class="container py-5">
<div class="shadow p-4 rounded bg-light">
<h2 class="mb-4 text-center"> <?= $translations['mes_praticiens_favoris_heading'] ?></h2>
<h2 class="mb-4 text-center"> <?php echo $translations['mes_praticiens_favoris_heading'] ?></h2>
<?php if (empty($favoris)): ?>
<div class="alert alert-info"><?= $translations['aucun_favori_pour_le_moment'] ?></div>
<div class="alert alert-info"><?php echo $translations['aucun_favori_pour_le_moment'] ?></div>
<?php else: ?>
<?php foreach ($favoris as $p): ?>
<div class="card mb-4 shadow-sm" id="praticien_<?= $p['id'] ?>">
<div class="card mb-4 shadow-sm" id="praticien_<?php echo $p['id'] ?>">
<div class="card-body praticien-card">
<div>
<?php if ($p['photo']): ?>
<img src="/public/uploads/<?= htmlspecialchars($p['photo']) ?>" alt="<?= $translations['alt_photo'] ?>" class="card-img-top">
<img src="/public/uploads/<?php echo htmlspecialchars($p['photo']) ?>" alt="<?php echo $translations['alt_photo'] ?>" class="card-img-top">
<?php else: ?>
<img src="/public/uploads/img/user_placeholder.png" alt="<?= $translations['alt_photo'] ?>" class="card-img-top">
<img src="/public/uploads/img/user_placeholder.png" alt="<?php echo $translations['alt_photo'] ?>" class="card-img-top">
<?php endif; ?>
</div>
<div class="card-body p-0">
<h2 class="pro-name"><?= htmlspecialchars($p['prenom']?? '') . ' ' . htmlspecialchars($p['nom']?? '') ?></h5>
<p class="mb-1"><strong><?= $translations['label_ville'] ?> :</strong> <?= htmlspecialchars($p['ville'] ?? '') ?></p>
<h2 class="pro-name"><?php echo htmlspecialchars($p['prenom'] ?? '') . ' ' . htmlspecialchars($p['nom'] ?? '') ?></h5>
<p class="mb-1"><strong><?php echo $translations['label_ville'] ?> :</strong> <?php echo htmlspecialchars($p['ville'] ?? '') ?></p>
<p class="mb-2">
<?php if (!empty($p['presentation'])): ?>
<?= h(excerpt($p['presentation'], 300)) ?>
<?php if (! empty($p['presentation'])): ?>
<?php echo h(excerpt($p['presentation'], 300)) ?>
<?php endif; ?>
</p>
<div class="d-flex gap-3">
<button class="btn btn-danger btn-sm" onclick="supprimerFavori(<?= $p['id'] ?>)"> <?= $translations['btn_retirer'] ?></button>
<a class="btn btn-outline-secondary btn-sm btn-table" href="/public/client/reserver_rdv.php?praticien_id=<?= $p['id'] ?>"> <?= $translations['btn_rendez_vous'] ?></a>
<button class="btn btn-danger btn-sm" onclick="supprimerFavori(<?php echo $p['id'] ?>)"> <?php echo $translations['btn_retirer'] ?></button>
<a class="btn btn-outline-secondary btn-sm btn-table" href="/public/client/reserver_rdv.php?praticien_id=<?php echo $p['id'] ?>"> <?php echo $translations['btn_rendez_vous'] ?></a>
</div>
</div>
</div>
@@ -70,44 +71,76 @@ require_once '../includes/header.php';
<?php endif; ?>
</div>
</div>
<script>
const csrfToken = <?php echo json_encode(ensure_csrf_token(), JSON_UNESCAPED_UNICODE) ?>;
</script>
<script>
function supprimerFavori(praticienId) {
Swal.fire({
title: <?= json_encode($translations['confirm_supprimer_favori_title']) ?>,
text: <?= json_encode($translations['confirm_supprimer_favori_text']) ?>,
title: <?php echo json_encode($translations['confirm_supprimer_favori_title']) ?>,
text: <?php echo json_encode($translations['confirm_supprimer_favori_text']) ?>,
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#dc3545',
cancelButtonColor: '#6c757d',
confirmButtonText: <?= json_encode($translations['btn_oui_supprimer']) ?>,
cancelButtonText: <?= json_encode($translations['btn_annuler']) ?>
confirmButtonText: <?php echo json_encode($translations['btn_oui_supprimer']) ?>,
cancelButtonText: <?php echo json_encode($translations['btn_annuler']) ?>
}).then((result) => {
if (result.isConfirmed) {
fetch('/public/client/supprimer_favori.php', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'praticien_id=' + praticienId
})
.then(res => res.text())
.then(msg => {
Swal.fire({
icon: 'success',
title: <?= json_encode($translations['favori_supprime_title']) ?>,
text: msg,
timer: 1500,
showConfirmButton: false
});
const element = document.getElementById('praticien_' + praticienId);
if (element) element.remove();
if (!result.isConfirmed) return;
const formData = new FormData();
formData.append('praticien_id', String(praticienId));
formData.append('csrf_token', csrfToken);
fetch('/public/client/supprimer_favori.php', {
method: 'POST',
headers: {
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: formData
})
.then(async (res) => {
const text = await res.text();
let data;
try {
data = JSON.parse(text);
} catch (e) {
throw new Error(text || 'Erreur réseau');
}
if (!res.ok || !data.ok) {
throw new Error(data.message || 'Erreur.');
}
Swal.fire({
icon: 'success',
title: <?php echo json_encode($translations['favori_supprime_title']) ?>,
text: data.message || '',
timer: 1500,
showConfirmButton: false
});
}
const element = document.getElementById('praticien_' + praticienId);
if (element) {
element.remove();
}
})
.catch((err) => {
Swal.fire({
icon: 'error',
title: 'Oups',
text: err.message || 'Erreur.'
});
});
});
}
</script>
</div>
<footer class="site-footer">
<div class="container">
<p><?= $translations['copyr'] ?></p>
<p><?php echo $translations['copyr'] ?></p>
</div>
</footer>
</body>
+100 -96
View File
@@ -1,21 +1,17 @@
<?php
require_once __DIR__ . '/../includes/init_client.php';
require_once __DIR__ . '/../includes/init_client.php';
$pdo->prepare("
$pdo->prepare("
INSERT INTO push_badge_counters (compte_id, msg_unread, rdv_unread, updated_at)
VALUES (:cid, 0, 0, NOW())
ON DUPLICATE KEY UPDATE
rdv_unread = 0,
updated_at = NOW()
")->execute([':cid' => (int)$_SESSION['compte_id']]);
")->execute([':cid' => (int) $_SESSION['compte_id']]);
$client_id = $_SESSION['reference_id'];
$client_id = $_SESSION['reference_id'];
$stmt = $pdo->prepare("
$stmt = $pdo->prepare("
SELECT
rv.*,
p.nom AS nom_praticien,
@@ -29,143 +25,148 @@ $stmt = $pdo->prepare("
WHERE rv.client_id = ? AND rv.date_rdv >= NOW()
ORDER BY rv.date_rdv ASC
");
$stmt->execute([$client_id]);
$rdvs = $stmt->fetchAll(PDO::FETCH_ASSOC);
$stmt->execute([$client_id]);
$rdvs = $stmt->fetchAll(PDO::FETCH_ASSOC);
$PAGE_TITLE = $translations['mes_rdv_title'] ?? 'Mes rendez-vous';
$HEAD_EXTRA = <<<HTML
$PAGE_TITLE = $translations['mes_rdv_title'] ?? 'Mes rendez-vous';
$HEAD_EXTRA = <<<HTML
<script src="/js/sweetalert2@11.js"></script>
HTML;
require_once '../includes/header.php';
require_once '../includes/header.php';
?>
<div class="main1">
<div class="container py-5">
<div class="shadow p-4 rounded bg-light">
<h2 class="mb-4 text-center">📅 <?= $translations['mes_rdv_heading'] ?></h2>
<h2 class="mb-4 text-center">📅 <?php echo $translations['mes_rdv_heading'] ?></h2>
<div class="table-responsive">
<table class="table table-striped table-hover align-middle mobile-cards" id="table_rdv">
<thead class="table-light">
<tr align="center">
<th><?= $translations['col_date'] ?></th>
<th><?= $translations['col_heure'] ?></th>
<th><?= $translations['col_praticien'] ?></th>
<th><?= $translations['col_duree'] ?></th>
<th><?= $translations['col_statut'] ?></th>
<th><?= $translations['col_action'] ?></th>
<th><?php echo $translations['col_date'] ?></th>
<th><?php echo $translations['col_heure'] ?></th>
<th><?php echo $translations['col_praticien'] ?></th>
<th><?php echo $translations['col_duree'] ?></th>
<th><?php echo $translations['col_statut'] ?></th>
<th><?php echo $translations['col_action'] ?></th>
<!-- Chat -->
<th><?= $translations['btn_chat'] ?></th>
<th><?php echo $translations['btn_chat'] ?></th>
<!-- Visio / Adresse -->
<th><?= htmlspecialchars($translations['status_online']) ?></th>
<th><?php echo htmlspecialchars($translations['status_online']) ?></th>
</tr>
</thead>
<tbody>
<?php if (empty($rdvs)): ?>
<tr><td colspan="8" class="text-center"><?= $translations['aucun_rdv_a_venir'] ?></td></tr>
<tr><td colspan="8" class="text-center"><?php echo $translations['aucun_rdv_a_venir'] ?></td></tr>
<?php else: ?>
<?php foreach ($rdvs as $r):
$dt = new DateTime($r['date_rdv']);
$id = (int)$r['id'];
$ficheUrl = '/specialiste?id='.(int)$r['praticien_id'];
$dt = new DateTime($r['date_rdv']);
$id = (int) $r['id'];
$ficheUrl = '/specialiste?id=' . (int) $r['praticien_id'];
?>
<tr>
<td><?= $dt->format('d/m/Y') ?></td>
<td><?= $dt->format('H:i') ?></td>
<td><?php echo $dt->format('d/m/Y') ?></td>
<td><?php echo $dt->format('H:i') ?></td>
<td>
<a href="/public/client/detail_praticien.php?id=<?= (int)$r['praticien_id'] ?>">
<?= htmlspecialchars($r['prenom_praticien'] . ' ' . $r['nom_praticien']) ?>
<a href="/public/client/detail_praticien.php?id=<?php echo (int) $r['praticien_id'] ?>">
<?php echo htmlspecialchars($r['prenom_praticien'] . ' ' . $r['nom_praticien']) ?>
</a>
</td>
<td><?= (int)$r['duree_minutes'] ?> <?= $translations['min_abbr'] ?></td>
<td><?php echo (int) $r['duree_minutes'] ?> <?php echo $translations['min_abbr'] ?></td>
<td id="statut_<?= $id ?>">
<span class="badge rounded-pill <?= $r['statut'] === 'confirmé' ? 'bg-success' : ($r['statut'] === 'prévu' ? 'bg-warning text-dark' : 'bg-secondary') ?>">
<?= $r['statut'] === 'confirmé' ? $translations['statut_confirme'] : ($r['statut'] === 'prévu' ? $translations['statut_prevu'] : $translations['statut_annule']) ?>
<td id="statut_<?php echo $id ?>">
<span class="badge rounded-pill <?php echo $r['statut'] === 'confirmé' ? 'bg-success' : ($r['statut'] === 'prévu' ? 'bg-warning text-dark' : 'bg-secondary') ?>">
<?php echo $r['statut'] === 'confirmé' ? $translations['statut_confirme'] : ($r['statut'] === 'prévu' ? $translations['statut_prevu'] : $translations['statut_annule']) ?>
</span>
</td>
<td>
<?php if ($r['statut'] !== 'annulé'): ?>
<button class="btn btn-outline-danger btn-sm" onclick="annulerRdv(<?= $id ?>)"> <?= $translations['btn_annuler'] ?></button>
<button class="btn btn-outline-danger btn-sm" onclick="annulerRdv(<?php echo $id ?>)"> <?php echo $translations['btn_annuler'] ?></button>
<?php else: ?>
<em class="text-muted"><?= $translations['statut_annule'] ?></em>
<em class="text-muted"><?php echo $translations['statut_annule'] ?></em>
<?php endif; ?>
</td>
<!-- Chat -->
<td>
<a class="btn btn-outline-secondary btn-sm btn-table" href="/messages/start.php?praticien_id=<?= (int)$r['praticien_id'] ?>">
<?= $translations['chat_praticien'] ?>
<a class="btn btn-outline-secondary btn-sm btn-table" href="/messages/start.php?praticien_id=<?php echo (int) $r['praticien_id'] ?>">
<?php echo $translations['chat_praticien'] ?>
</a>
</td>
<!-- Visio (online) OU Adresse/Lien (offline) -->
<td>
<?php
$lieu = strtolower(trim((string)($r['lieu'] ?? '')));
if ($lieu === 'online') {
// --- Affichage visio comme avant ---
$statut = $r['statut']; // 'confirmé' | 'prévu' | 'annulé'
$lieu = strtolower(trim((string) ($r['lieu'] ?? '')));
if ($lieu === 'online') {
// --- Affichage visio comme avant ---
$statut = $r['statut']; // 'confirmé' | 'prévu' | 'annulé'
$tzParis = new DateTimeZone('Europe/Paris');
$now = new DateTime('now', $tzParis);
$start = new DateTime($r['date_rdv'], $tzParis);
$open = (clone $start)->modify('-15 minutes');
$close = (clone $start)->modify('+'.(int)($r['duree_minutes'] ?? 120).' minutes');
$tzParis = new DateTimeZone('Europe/Paris');
$now = new DateTime('now', $tzParis);
$start = new DateTime($r['date_rdv'], $tzParis);
$open = (clone $start)->modify('-15 minutes');
$close = (clone $start)->modify('+' . (int) ($r['duree_minutes'] ?? 120) . ' minutes');
if ($statut === 'confirmé') {
if ($now >= $open && $now <= $close) {
// Bouton actif
?>
<a href="/visio/join.php?rdv=<?= (int)$r['id'] ?>&start=1" class="btn btn-outline-secondary btn-sm btn-table">
<?= htmlspecialchars($translations['visio_nachalo']) ?>
if ($statut === 'confirmé') {
if ($now >= $open && $now <= $close) {
// Bouton actif
?>
<a href="/visio/join.php?rdv=<?php echo (int) $r['id'] ?>&start=1" class="btn btn-outline-secondary btn-sm btn-table">
<?php echo htmlspecialchars($translations['visio_nachalo']) ?>
</a>
<?php
} else {
// Messages hors fenêtre
?>
} else {
// Messages hors fenêtre
?>
<small class="text-muted d-block mt-1">
<?php
if ($now < $open) echo htmlspecialchars(($translations['ouvert_a'] ?? 'Откроется в')).' '.$open->format('H:i');
elseif ($now > $close) echo htmlspecialchars(($translations['ferme_a'] ?? 'Закрыто в')).' '.$close->format('H:i');
else echo htmlspecialchars(($translations['prev_a'] ?? 'Запланировано на')).' '.$start->format('d/m/Y \à H:i').'.';
?>
if ($now < $open) {
echo htmlspecialchars(($translations['ouvert_a'] ?? 'Откроется в')) . ' ' . $open->format('H:i');
} elseif ($now > $close) {
echo htmlspecialchars(($translations['ferme_a'] ?? 'Закрыто в')) . ' ' . $close->format('H:i');
} else {
echo htmlspecialchars(($translations['prev_a'] ?? 'Запланировано на')) . ' ' . $start->format('d/m/Y \à H:i') . '.';
}
?>
</small>
<?php
}
} elseif ($statut === 'prévu') {
// Pas encore confirmé
?>
}
} elseif ($statut === 'prévu') {
// Pas encore confirmé
?>
<small class="text-danger d-block mt-1">
<?= htmlspecialchars($translations['votre_rdv_jdet'] ?? 'En attente de confirmation') ?>
<?php echo htmlspecialchars($translations['votre_rdv_jdet'] ?? 'En attente de confirmation') ?>
</small>
<?php
} else {
?>
} else {
?>
<small class="text-muted d-block mt-1">
<?= htmlspecialchars($translations['statut_annule'] ?? 'Annulé') ?>
<?php echo htmlspecialchars($translations['statut_annule'] ?? 'Annulé') ?>
</small>
<?php
}
}
} else {
// --- OFFLINE : afficher adresse (si dispo) + lien fiche praticien ---
$addrParts = array_filter([
trim((string)($r['p_adresse'] ?? '')),
trim((string)($r['p_cp'] ?? '')) . ' ' . trim((string)($r['p_ville'] ?? ''))
], fn($x) => $x !== '' && $x !== ' ');
$addr = trim(implode(', ', $addrParts));
} else {
// --- OFFLINE : afficher adresse (si dispo) + lien fiche praticien ---
$addrParts = array_filter([
trim((string) ($r['p_adresse'] ?? '')),
trim((string) ($r['p_cp'] ?? '')) . ' ' . trim((string) ($r['p_ville'] ?? '')),
], fn($x) => $x !== '' && $x !== ' ');
$addr = trim(implode(', ', $addrParts));
if ($addr !== '') {
//echo '<div class="mb-1"> '.htmlspecialchars($addr).'</div>';
}
?>
<a href="<?= htmlspecialchars($ficheUrl) ?>" class="btn btn-outline-secondary btn-sm btn-table">
<?= htmlspecialchars($translations['voir_fiche_praticien'] ?? 'Voir la fiche') ?>
if ($addr !== '') {
//echo '<div class="mb-1"> '.htmlspecialchars($addr).'</div>';
}
?>
<a href="<?php echo htmlspecialchars($ficheUrl) ?>" class="btn btn-outline-secondary btn-sm btn-table">
<?php echo htmlspecialchars($translations['voir_fiche_praticien'] ?? 'Voir la fiche') ?>
</a>
<?php
}
?>
}
?>
</td>
</tr>
<?php endforeach; ?>
@@ -175,32 +176,35 @@ require_once '../includes/header.php';
</div>
</div>
</div>
<script>
const csrfToken = <?php echo json_encode(ensure_csrf_token(), JSON_UNESCAPED_UNICODE) ?>;
function annulerRdv(id) {
Swal.fire({
title: <?= json_encode($translations['confirm_annulation_title']) ?>,
title: <?php echo json_encode($translations['confirm_annulation_title']) ?>,
icon: 'warning',
showCancelButton: true,
confirmButtonText: <?= json_encode($translations['btn_oui_annuler']) ?>,
cancelButtonText: <?= json_encode($translations['btn_annuler']) ?>,
confirmButtonText: <?php echo json_encode($translations['btn_oui_annuler']) ?>,
cancelButtonText: <?php echo json_encode($translations['btn_annuler']) ?>,
confirmButtonColor: '#dc3545',
cancelButtonColor: '#6c757d'
}).then((result) => {
if (!result.isConfirmed) return;
fetch('/public/client/annuler_rdv.php', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'},
body: new URLSearchParams({ id: String(id) })
})
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'},
body: new URLSearchParams({
id: String(id),
csrf_token: csrfToken
})
})
.then(r => r.json().catch(() => ({ok:false, message:'Bad JSON'})))
.then(data => {
if (data.ok) {
Swal.fire({ icon:'success', title: <?= json_encode($translations['rdv_annule_ok']) ?>, text: data.message, timer: 1600, showConfirmButton:false });
Swal.fire({ icon:'success', title: <?php echo json_encode($translations['rdv_annule_ok']) ?>, text: data.message, timer: 1600, showConfirmButton:false });
const statutEl = document.getElementById("statut_" + id);
if (statutEl) {
statutEl.innerHTML = '<span class="badge bg-secondary"><?= htmlspecialchars($translations['statut_annule']) ?></span>';
statutEl.innerHTML = '<span class="badge bg-secondary"><?php echo htmlspecialchars($translations['statut_annule']) ?></span>';
}
} else {
Swal.fire({ icon:'error', title: 'Oops', text: data.message || 'Erreur' });
@@ -241,7 +245,7 @@ function annulerRdv(id) {
</div>
<footer class="site-footer">
<div class="container">
<p><?= $translations['copyr'] ?></p>
<p><?php echo $translations['copyr'] ?></p>
</div>
</footer>
</body>
+48 -46
View File
@@ -1,19 +1,20 @@
<?php
require_once __DIR__ . '/../includes/init_client.php';
require_once __DIR__ . '/../includes/init_client.php';
$id = (int)($_SESSION['reference_id'] ?? 0);
if (!$id) { http_response_code(403); exit($translations['client_introuvable'] ?? 'Client introuvable'); }
$id = (int) ($_SESSION['reference_id'] ?? 0);
if (! $id) {http_response_code(403);exit($translations['client_introuvable'] ?? 'Client introuvable');}
// Récupérer les données du client
$stmt = $pdo->prepare("SELECT * FROM client WHERE id = ?");
$stmt->execute([$id]);
$client = $stmt->fetch();
if (!$client) {
// Récupérer les données du client
$stmt = $pdo->prepare("SELECT * FROM client WHERE id = ?");
$stmt->execute([$id]);
$client = $stmt->fetch();
if (! $client) {
exit($translations['client_introuvable'] ?? 'Client introuvable');
}
}
// ---- Traitement du formulaire AVANT tout output ----
if (($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
// ---- Traitement du formulaire AVANT tout output ----
if (($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
require_post_csrf();
$nom = trim($_POST['nom'] ?? '');
$prenom = trim($_POST['prenom'] ?? '');
$telephone = trim($_POST['telephone'] ?? '');
@@ -30,84 +31,85 @@ if (($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
WHERE id=:id
");
$stmt->execute([
'nom' => $nom,
'prenom' => $prenom,
'telephone' => $telephone,
'genre' => $genre,
'nom' => $nom,
'prenom' => $prenom,
'telephone' => $telephone,
'genre' => $genre,
'date_naissance' => $date_naiss ?: null,
'adresse' => $adresse,
'ville' => $ville,
'code_postal' => $cp,
'id' => $id
'adresse' => $adresse,
'ville' => $ville,
'code_postal' => $cp,
'id' => $id,
]);
// redirection (évite le repost + actualise la page infos)
header('Location: /public/client/informations.php?updated=1');
exit;
}
require_once __DIR__ . '/../includes/header.php';
}
require_once __DIR__ . '/../includes/header.php';
?>
<div class="main1">
<div class="container py-5">
<form method="POST" class="shadow p-4 rounded bg-light">
<h2 class="mb-4 text-center"><?= $translations['modifier_infos_heading'] ?></h2>
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(ensure_csrf_token(), ENT_QUOTES, 'UTF-8') ?>">
<h2 class="mb-4 text-center"><?php echo $translations['modifier_infos_heading'] ?></h2>
<div class="mb-3">
<label class="form-label"><?= $translations['label_nom'] ?> *</label>
<input type="text" name="nom" value="<?= htmlspecialchars($client['nom'] ?? '') ?>" required class="form-control">
<label class="form-label"><?php echo $translations['label_nom'] ?> *</label>
<input type="text" name="nom" value="<?php echo htmlspecialchars($client['nom'] ?? '') ?>" required class="form-control">
</div>
<div class="mb-3">
<label class="form-label"><?= $translations['label_prenom'] ?> *</label>
<input type="text" name="prenom" value="<?= htmlspecialchars($client['prenom'] ?? '') ?>" required class="form-control">
<label class="form-label"><?php echo $translations['label_prenom'] ?> *</label>
<input type="text" name="prenom" value="<?php echo htmlspecialchars($client['prenom'] ?? '') ?>" required class="form-control">
</div>
<div class="mb-3">
<label class="form-label"><?= $translations['label_telephone'] ?> *</label>
<input type="tel" name="telephone" id="telephone" value="<?= htmlspecialchars($client['telephone'] ?? '') ?>" class="form-control" placeholder="+33600000000"
<label class="form-label"><?php echo $translations['label_telephone'] ?> *</label>
<input type="tel" name="telephone" id="telephone" value="<?php echo htmlspecialchars($client['telephone'] ?? '') ?>" class="form-control" placeholder="+33600000000"
required
pattern="^\+?[1-9]\d{6,14}$"
title="<?=htmlspecialchars($translations['telephone_format_message']?? '') ?>">
title="<?php echo htmlspecialchars($translations['telephone_format_message'] ?? '') ?>">
</div>
<div class="mb-3">
<label class="form-label"><?= $translations['label_genre'] ?></label>
<label class="form-label"><?php echo $translations['label_genre'] ?></label>
<select name="genre" class="form-select">
<option value=""><?= $translations['select_selectionnez'] ?></option>
<option value="<?= $translations['genre_homme'] ?>" <?= $client['genre'] === $translations['genre_homme'] ? 'selected' : '' ?>><?= $translations['genre_homme'] ?></option>
<option value="<?= $translations['genre_femme'] ?>" <?= $client['genre'] === $translations['genre_femme'] ? 'selected' : '' ?>><?= $translations['genre_femme'] ?></option>
<option value="<?= $translations['genre_autre'] ?>" <?= $client['genre'] === $translations['genre_autre'] ? 'selected' : '' ?>><?= $translations['genre_autre'] ?></option>
<option value=""><?php echo $translations['select_selectionnez'] ?></option>
<option value="<?php echo $translations['genre_homme'] ?>" <?php echo $client['genre'] === $translations['genre_homme'] ? 'selected' : '' ?>><?php echo $translations['genre_homme'] ?></option>
<option value="<?php echo $translations['genre_femme'] ?>" <?php echo $client['genre'] === $translations['genre_femme'] ? 'selected' : '' ?>><?php echo $translations['genre_femme'] ?></option>
<option value="<?php echo $translations['genre_autre'] ?>" <?php echo $client['genre'] === $translations['genre_autre'] ? 'selected' : '' ?>><?php echo $translations['genre_autre'] ?></option>
</select>
</div>
<div class="mb-3">
<label class="form-label"><?= $translations['label_date_naissance'] ?></label>
<input type="date" name="date_naissance" value="<?= htmlspecialchars($client['date_naissance'] ?? '') ?>" class="form-control">
<label class="form-label"><?php echo $translations['label_date_naissance'] ?></label>
<input type="date" name="date_naissance" value="<?php echo htmlspecialchars($client['date_naissance'] ?? '') ?>" class="form-control">
</div>
<div class="mb-3 input-wrapper">
<label class="form-label"><?= $translations['label_adresse'] ?></label>
<textarea name="adresse" id="adresse" class="form-control"><?= htmlspecialchars($client['adresse'] ?? '') ?></textarea>
<label class="form-label"><?php echo $translations['label_adresse'] ?></label>
<textarea name="adresse" id="adresse" class="form-control"><?php echo htmlspecialchars($client['adresse'] ?? '') ?></textarea>
<ul id="suggestions" class="list-group position-absolute"></ul>
</div>
<div class="mb-3">
<label class="form-label"><?= $translations['label_ville'] ?></label>
<input type="text" name="ville" id="ville" value="<?= htmlspecialchars($client['ville'] ?? '') ?>" class="form-control">
<label class="form-label"><?php echo $translations['label_ville'] ?></label>
<input type="text" name="ville" id="ville" value="<?php echo htmlspecialchars($client['ville'] ?? '') ?>" class="form-control">
</div>
<div class="mb-4">
<label class="form-label"><?= $translations['label_code_postal'] ?></label>
<input type="text" name="code_postal" id="code_postal" value="<?= htmlspecialchars($client['code_postal'] ?? '') ?>" class="form-control">
<label class="form-label"><?php echo $translations['label_code_postal'] ?></label>
<input type="text" name="code_postal" id="code_postal" value="<?php echo htmlspecialchars($client['code_postal'] ?? '') ?>" class="form-control">
</div>
<div class="text-center">
<button type="submit" class="btn btn-primary"><?= $translations['btn_mettre_a_jour'] ?></button>
<button type="submit" class="btn btn-primary"><?php echo $translations['btn_mettre_a_jour'] ?></button>
</div>
<?= $translations['champs_obligatoire'] ?>
<?php echo $translations['champs_obligatoire'] ?>
</form>
</div>
@@ -217,7 +219,7 @@ document.addEventListener("DOMContentLoaded", function () {
</div>
<footer class="site-footer">
<div class="container">
<p><?= $translations['copyr'] ?></p>
<p><?php echo $translations['copyr'] ?></p>
</div>
</footer>
</body>
+53 -9
View File
@@ -1,15 +1,59 @@
<?php
declare (strict_types = 1);
require_once __DIR__ . '/../includes/init_client.php';
$client_id = $_SESSION['reference_id'];
$praticien_id = intval($_POST['praticien_id'] ?? 0);
header('Content-Type: application/json; charset=utf-8');
if ($praticien_id > 0) {
$stmt = $pdo->prepare("DELETE FROM praticien_client WHERE client_id = ? AND praticien_id = ?");
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
echo json_encode([
'ok' => false,
'message' => 'Méthode non autorisée.',
], JSON_UNESCAPED_UNICODE);
exit;
}
require_post_csrf();
$client_id = (int) ($_SESSION['reference_id'] ?? 0);
$praticien_id = (int) ($_POST['praticien_id'] ?? 0);
if ($client_id <= 0) {
echo json_encode([
'ok' => false,
'message' => $translations['erreur_generique'] ?? 'Erreur.',
], JSON_UNESCAPED_UNICODE);
exit;
}
if ($praticien_id <= 0) {
echo json_encode([
'ok' => false,
'message' => $translations['praticien_id_invalide'] ?? 'ID praticien invalide.',
], JSON_UNESCAPED_UNICODE);
exit;
}
try {
$stmt = $pdo->prepare("
DELETE FROM praticien_client
WHERE client_id = ? AND praticien_id = ?
");
$stmt->execute([$client_id, $praticien_id]);
echo $translations['favori_supprime_title'];
} else {
echo $translations['praticien_id_invalide'];
echo json_encode([
'ok' => true,
'deleted' => ($stmt->rowCount() > 0),
'message' => $translations['favori_supprime_title'] ?? 'Favori supprimé.',
], JSON_UNESCAPED_UNICODE);
exit;
} catch (Throwable $e) {
error_log('supprimer_favori.php: ' . $e->getMessage());
echo json_encode([
'ok' => false,
'message' => $translations['erreur_generique'] ?? 'Erreur serveur.',
], JSON_UNESCAPED_UNICODE);
exit;
}
?>
+8 -3
View File
@@ -292,9 +292,14 @@
<li onclick="closeMenu(); openContactAdmin()">
<?php echo htmlspecialchars($translations['contact_admin']) ?>
</li>
<li onclick="location.href='/public/deconnexion.php'">
<?php echo htmlspecialchars($translations['menu_deconnexion']) ?>
</li>
<li class="menu-item-form">
<form method="POST" action="/public/deconnexion.php" class="menu-logout-form">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(ensure_csrf_token(), ENT_QUOTES, 'UTF-8') ?>">
<button type="submit" class="menu-logout-btn">
<?php echo htmlspecialchars($translations['menu_deconnexion'] ?? 'Déconnexion') ?>
</button>
</form>
</li>
</ul>
</div>
</nav>
+101 -95
View File
@@ -1,59 +1,58 @@
<?php
// ⚠️ NE PAS inclure contact_admin.php ici (risque d'exécution à l'inclusion)
require_once __DIR__ . '/../contact_admin.php';
// ⚠️ NE PAS inclure contact_admin.php ici (risque d'exécution à l'inclusion)
require_once __DIR__ . '/../contact_admin.php';
$sec_uid = (int) ($_SESSION['compte_id'] ?? $_SESSION['user_id'] ?? 0);
$sec_uid = (int)($_SESSION['compte_id'] ?? $_SESSION['user_id'] ?? 0);
// =====================================================
// [CONTEXT UTILISATEUR] — Initiales avatar secrétaire
// =====================================================
$prenomUser = trim((string) ($_SESSION['user_prenom'] ?? $_SESSION['prenom'] ?? ''));
$nomUser = trim((string) ($_SESSION['user_nom'] ?? $_SESSION['nom'] ?? ''));
// =====================================================
// [CONTEXT UTILISATEUR] — Initiales avatar secrétaire
// =====================================================
$prenomUser = trim((string)($_SESSION['user_prenom'] ?? $_SESSION['prenom'] ?? ''));
$nomUser = trim((string)($_SESSION['user_nom'] ?? $_SESSION['nom'] ?? ''));
if ($prenomUser === '' && $nomUser === '') {
// petit fallback
$prenomUser = 'Secr';
$nomUser = 'Etaire';
}
$initiales = mb_strtoupper(
mb_substr($prenomUser, 0, 1, 'UTF-8') .
mb_substr($nomUser, 0, 1, 'UTF-8'),
'UTF-8'
);
if ($prenomUser === '' && $nomUser === '') {
// petit fallback
$prenomUser = 'Secr';
$nomUser = 'Etaire';
}
$initiales = mb_strtoupper(
mb_substr($prenomUser, 0, 1, 'UTF-8') .
mb_substr($nomUser, 0, 1, 'UTF-8'),
'UTF-8'
);
// =====================================================
// [PRATICIEN COURANT] — affichage nom/prénom dans le header
// - on prend ?praticien_id si présent, sinon une valeur de session si dispo
// =====================================================
$currentPraticienId = null;
if (isset($_GET['praticien_id'])) {
$currentPraticienId = (int) $_GET['praticien_id'];
$_SESSION['current_praticien_id'] = $currentPraticienId; // mémorise pour les autres pages
} elseif (! empty($_SESSION['current_praticien_id'])) {
$currentPraticienId = (int) $_SESSION['current_praticien_id'];
}
// =====================================================
// [PRATICIEN COURANT] — affichage nom/prénom dans le header
// - on prend ?praticien_id si présent, sinon une valeur de session si dispo
// =====================================================
$currentPraticienId = null;
if (isset($_GET['praticien_id'])) {
$currentPraticienId = (int)$_GET['praticien_id'];
$_SESSION['current_praticien_id'] = $currentPraticienId; // mémorise pour les autres pages
} elseif (!empty($_SESSION['current_praticien_id'])) {
$currentPraticienId = (int)$_SESSION['current_praticien_id'];
}
$currentPraticienName = '';
if ($currentPraticienId) {
$stmt = $pdo->prepare("SELECT id, prenom, nom FROM praticien WHERE id = ? LIMIT 1");
$stmt->execute([$currentPraticienId]);
if ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$p = trim((string) ($row['prenom'] ?? ''));
$n = trim((string) ($row['nom'] ?? ''));
$pid = trim((string) ($row['id'] ?? ''));
$currentPraticienName = trim($p . ' ' . $n);
}
}
$currentPraticienName = '';
if ($currentPraticienId) {
$stmt = $pdo->prepare("SELECT id, prenom, nom FROM praticien WHERE id = ? LIMIT 1");
$stmt->execute([$currentPraticienId]);
if ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$p = trim((string)($row['prenom'] ?? ''));
$n = trim((string)($row['nom'] ?? ''));
$pid = trim((string)($row['id'] ?? ''));
$currentPraticienName = trim($p.' '.$n);
}
}
// =====================================================
// [Liste Praticien avec autorisations]
// =====================================================
$pid = (int) $currentPraticienId;
$permRow = null;
// =====================================================
// [Liste Praticien avec autorisations]
// =====================================================
$pid = (int)$currentPraticienId;
$permRow = null;
if ($pid && $sec_uid) {
$q = $pdo->prepare("
if ($pid && $sec_uid) {
$q = $pdo->prepare("
SELECT sp.can_manage_rdv, sp.can_edit_slots
FROM secretaire_praticien sp
WHERE sp.secretaire_user_id = :u
@@ -61,48 +60,50 @@ if ($pid && $sec_uid) {
AND sp.is_active = 1
LIMIT 1
");
$q->execute(['u' => $sec_uid, 'pid' => $pid]);
$permRow = $q->fetch(PDO::FETCH_ASSOC) ?: null;
}
$q->execute(['u' => $sec_uid, 'pid' => $pid]);
$permRow = $q->fetch(PDO::FETCH_ASSOC) ?: null;
}
// =====================================================
// [RETOUR / REFERER SÉCURISÉ]
// =====================================================
$ref = $_SERVER['HTTP_REFERER'] ?? '';
$host = $_SERVER['HTTP_HOST'] ?? '';
$safeRef = '';
if ($ref) {
$u = parse_url($ref);
if (! empty($u['host']) && $u['host'] === $host) {
$safeRef = $ref;
}
// =====================================================
// [RETOUR / REFERER SÉCURISÉ]
// =====================================================
$ref = $_SERVER['HTTP_REFERER'] ?? '';
$host = $_SERVER['HTTP_HOST'] ?? '';
$safeRef = '';
if ($ref) {
$u = parse_url($ref);
if (!empty($u['host']) && $u['host'] === $host) $safeRef = $ref;
}
$fallbackBack = $safeRef ?: '/public/cabinet/dashboard.php';
}
$fallbackBack = $safeRef ?: '/public/cabinet/dashboard.php';
// =====================================================
// [DETECTION MOBILE]
// =====================================================
$is_mobile = preg_match('/Mobile|Android|iPhone|iPad/i', $_SERVER['HTTP_USER_AGENT'] ?? '');
// =====================================================
// [DETECTION MOBILE]
// =====================================================
$is_mobile = preg_match('/Mobile|Android|iPhone|iPad/i', $_SERVER['HTTP_USER_AGENT'] ?? '');
?>
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title><?= htmlspecialchars($PAGE_TITLE ?? 'Cabinet', ENT_QUOTES, 'UTF-8') ?></title>
<?= $HEAD_EXTRA ?? '' ?>
<title><?php echo htmlspecialchars($PAGE_TITLE ?? 'Cabinet', ENT_QUOTES, 'UTF-8') ?></title>
<?php echo $HEAD_EXTRA ?? '' ?>
<!-- Montserrat -->
<!-- Bootstrap -->
<link href="/css/bootstrap.min.css" rel="stylesheet">
<!-- Styles (chemins corrigés) -->
<link rel="stylesheet" href="<?= asset('/includes/site_style.css') ?>">
<link rel="stylesheet" href="<?= asset('/includes/custom_style.css') ?>"> <!-- ta home -->
<link rel="stylesheet" href="<?= asset('/includes/admin_style.css') ?>">
<link rel="stylesheet" href="<?= asset('/includes/mobil_style.css') ?>">
<link rel="stylesheet" href="<?php echo asset('/includes/site_style.css') ?>">
<link rel="stylesheet" href="<?php echo asset('/includes/custom_style.css') ?>"> <!-- ta home -->
<link rel="stylesheet" href="<?php echo asset('/includes/admin_style.css') ?>">
<link rel="stylesheet" href="<?php echo asset('/includes/mobil_style.css') ?>">
</head>
<body>
@@ -113,11 +114,11 @@ $is_mobile = preg_match('/Mobile|Android|iPhone|iPad/i', $_SERVER['HTTP_USER_AGE
<div class="header-left">
<div class="brand">
<a
href="<?= htmlspecialchars($fallbackBack, ENT_QUOTES, 'UTF-8') ?>"
href="<?php echo htmlspecialchars($fallbackBack, ENT_QUOTES, 'UTF-8') ?>"
id="btnBack"
class="btn btn-light btn-circle me-2"
style="border-radius:5px; box-shadow:0 1px 2px rgba(0,0,0,.08);"
aria-label="<?= htmlspecialchars($translations['retour'] ?? 'Retour', ENT_QUOTES, 'UTF-8') ?>"
aria-label="<?php echo htmlspecialchars($translations['retour'] ?? 'Retour', ENT_QUOTES, 'UTF-8') ?>"
>
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" aria-hidden="true">
<path d="M15 6l-6 6 6 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
@@ -144,9 +145,9 @@ $is_mobile = preg_match('/Mobile|Android|iPhone|iPad/i', $_SERVER['HTTP_USER_AGE
<!-- ⚡ A LA PLACE DE L’ICÔNE MESSAGES : le praticien courant -->
<div class="text-nowrap" style="font-weight:600;">
<?php if ($currentPraticienName): ?>
<?= htmlspecialchars($currentPraticienName, ENT_QUOTES, 'UTF-8') ?>
<?php echo htmlspecialchars($currentPraticienName, ENT_QUOTES, 'UTF-8') ?>
<?php else: ?>
<span class="text-muted"><?= htmlspecialchars($translations['aucun_praticien_selectionne'] ?? 'Praticien non sélectionné', ENT_QUOTES, 'UTF-8') ?></span>
<span class="text-muted"><?php echo htmlspecialchars($translations['aucun_praticien_selectionne'] ?? 'Praticien non sélectionné', ENT_QUOTES, 'UTF-8') ?></span>
<?php endif; ?>
</div>
@@ -163,38 +164,43 @@ $is_mobile = preg_match('/Mobile|Android|iPhone|iPad/i', $_SERVER['HTTP_USER_AGE
<ul class="dropdown-menu-custom menu-panel" id="menu" style="display:none; min-width: 220px;">
<?php if (($_SESSION['role'] ?? '') === 'secretaire'): ?>
<li onclick="location.href='/public/cabinet/dashboard.php'">
<?= htmlspecialchars($translations['home'] ?? 'Accueil') ?>
<?php echo htmlspecialchars($translations['home'] ?? 'Accueil') ?>
</li>
<?php if ($pid && $permRow): ?>
<?php if (!empty($permRow['can_manage_rdv'])): ?>
<li onclick="location.href='/public/cabinet/agenda.php?praticien_id=<?= $pid ?>'">
<?= htmlspecialchars($translations['sec_btn_agenda'] ?? 'Agenda') ?>
<?php if (! empty($permRow['can_manage_rdv'])): ?>
<li onclick="location.href='/public/cabinet/agenda.php?praticien_id=<?php echo $pid ?>'">
<?php echo htmlspecialchars($translations['sec_btn_agenda'] ?? 'Agenda') ?>
</li>
<li onclick="location.href='/public/cabinet/rendez_vous_du_jour.php?praticien_id=<?= $pid ?>'">
<?= htmlspecialchars($translations['sec_btn_rdv'] ?? 'Rendez-vous') ?>
<li onclick="location.href='/public/cabinet/rendez_vous_du_jour.php?praticien_id=<?php echo $pid ?>'">
<?php echo htmlspecialchars($translations['sec_btn_rdv'] ?? 'Rendez-vous') ?>
</li>
<li onclick="location.href='/public/cabinet/rendez_vous.php?praticien_id=<?= $pid ?>'">
<?= htmlspecialchars($translations['sec_btn_rdv_jour'] ?? 'Les rendez-vous') ?>
<li onclick="location.href='/public/cabinet/rendez_vous.php?praticien_id=<?php echo $pid ?>'">
<?php echo htmlspecialchars($translations['sec_btn_rdv_jour'] ?? 'Les rendez-vous') ?>
</li>
<li onclick="location.href='/public/cabinet/reserver_rdv.php?praticien_id=<?= $pid ?>'">
<?= htmlspecialchars($translations['sec_btn_prendre'] ?? 'Prendre un RDV') ?>
<li onclick="location.href='/public/cabinet/reserver_rdv.php?praticien_id=<?php echo $pid ?>'">
<?php echo htmlspecialchars($translations['sec_btn_prendre'] ?? 'Prendre un RDV') ?>
</li>
<?php endif; ?>
<?php if (!empty($permRow['can_edit_slots'])): ?>
<li onclick="location.href='/public/cabinet/disponibilites.php?praticien_id=<?= $pid ?>'">
<?= htmlspecialchars($translations['sec_btn_dispos'] ?? 'Disponibilités') ?>
<?php if (! empty($permRow['can_edit_slots'])): ?>
<li onclick="location.href='/public/cabinet/disponibilites.php?praticien_id=<?php echo $pid ?>'">
<?php echo htmlspecialchars($translations['sec_btn_dispos'] ?? 'Disponibilités') ?>
</li>
<?php endif; ?>
<?php endif; ?>
<li onclick="closeMenu(); openContactAdmin()">
<?= htmlspecialchars($translations['contact_admin'] ?? 'Contact Admin') ?>
</li>
<li onclick="location.href='/public/deconnexion.php'">
<?= htmlspecialchars($translations['menu_deconnexion'] ?? 'Déconnexion') ?>
<?php echo htmlspecialchars($translations['contact_admin'] ?? 'Contact Admin') ?>
</li>
<li class="menu-item-form">
<form method="POST" action="/public/deconnexion.php" class="menu-logout-form">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(ensure_csrf_token(), ENT_QUOTES, 'UTF-8') ?>">
<button type="submit" class="menu-logout-btn">
<?php echo htmlspecialchars($translations['menu_deconnexion'] ?? 'Déconnexion') ?>
</button>
</form>
</li>
<?php endif; ?>
</ul>
+73 -46
View File
@@ -1,5 +1,5 @@
<?php
declare(strict_types=1);
declare (strict_types = 1);
// Interdit l’accès direct au fichier (pas de HTML ici)
if (isset($_SERVER['SCRIPT_FILENAME']) && realpath(__FILE__) === realpath($_SERVER['SCRIPT_FILENAME'])) {
@@ -8,16 +8,18 @@ if (isset($_SERVER['SCRIPT_FILENAME']) && realpath(__FILE__) === realpath($_SERV
}
/** Est-ce que l'utilisateur est connecté ? */
function is_logged_in(): bool {
function is_logged_in(): bool
{
return isset($_SESSION['compte_id'], $_SESSION['reference_id'])
&& $_SESSION['compte_id'] !== ''
&& $_SESSION['reference_id'] !== '';
}
/** Timeout inactivité (2h) */
function enforce_session_timeout(int $maxIdleSeconds = 7200): void {
$now = time();
$last = (int)($_SESSION['last_activity'] ?? 0);
function enforce_session_timeout(int $maxIdleSeconds = 7200): void
{
$now = time();
$last = (int) ($_SESSION['last_activity'] ?? 0);
if ($last > 0 && ($now - $last) > $maxIdleSeconds) {
logout_and_redirect_to_login($_SERVER['REQUEST_URI'] ?? '/');
@@ -26,59 +28,78 @@ function enforce_session_timeout(int $maxIdleSeconds = 7200): void {
}
/** Anti open-redirect : n’autorise que les chemins internes commençant par '/' */
function next_safe(?string $u): ?string {
if (!$u) return null;
if (!preg_match('#^/[^\\r\\n]*$#', $u)) return null;
if (strpos($u, '//') === 0) return null;
function next_safe(?string $u): ?string
{
if (! $u) {
return null;
}
if (! preg_match('#^/[^\\r\\n]*$#', $u)) {
return null;
}
if (strpos($u, '//') === 0) {
return null;
}
return $u;
}
function redirect_forbidden(?string $requestUri = null): void {
function redirect_forbidden(?string $requestUri = null): void
{
$u = $requestUri ?: ($_SERVER['REQUEST_URI'] ?? '/');
$u = next_safe($u) ?? '/';
header('Location: /403.php?next=' . rawurlencode($u), true, 303);
exit;
}
function redirect_to_login_with_next(?string $requestUri = null): void {
function redirect_to_login_with_next(?string $requestUri = null): void
{
$u = $requestUri ?: ($_SERVER['REQUEST_URI'] ?? '/');
$u = next_safe($u) ?? '/';
$_SESSION['redirect_to'] = $u;
header('Location: /connexion.php?next=' . rawurlencode($u), true, 303);
exit;
}
function logout_and_redirect_to_login(?string $requestUri = null): void {
function logout_and_redirect_to_login(): void
{
$_SESSION = [];
if (session_status() === PHP_SESSION_ACTIVE) {
$params = session_get_cookie_params();
setcookie(
session_name(),
'',
time() - 42000,
$params['path'] ?? '/',
$params['domain'] ?? '',
(bool)($params['secure'] ?? false),
(bool)($params['httponly'] ?? true)
);
}
if (session_status() === PHP_SESSION_ACTIVE) {
setcookie(session_name(), '', [
'expires' => time() - 42000,
'path' => $params['path'] ?? '/',
'domain' => $params['domain'] ?? '',
'secure' => (bool) ($params['secure'] ?? false),
'httponly' => (bool) ($params['httponly'] ?? true),
'samesite' => $params['samesite'] ?? 'Lax',
]);
session_destroy();
}
redirect_to_login_with_next($requestUri ?? ($_SERVER['REQUEST_URI'] ?? '/'));
header('Location: /index.php', true, 303);
exit;
}
/**
* 🔒 Re-check rôle depuis DB (optionnel) + hydrate $_SESSION['role'] si absent.
* Retourne le rôle (string) ou '' si introuvable.
*/
function refresh_role_from_db(PDO $pdo): string {
$compteId = (int)($_SESSION['compte_id'] ?? 0);
if ($compteId <= 0) return '';
function refresh_role_from_db(PDO $pdo): string
{
$compteId = (int) ($_SESSION['compte_id'] ?? 0);
if ($compteId <= 0) {
return '';
}
$stmt = $pdo->prepare("SELECT role FROM compte WHERE id=? LIMIT 1");
$stmt->execute([$compteId]);
$role = (string)($stmt->fetchColumn() ?: '');
$role = (string) ($stmt->fetchColumn() ?: '');
if ($role !== '') {
$_SESSION['role'] = $role;
}
@@ -93,26 +114,27 @@ function refresh_role_from_db(PDO $pdo): string {
* - 'recheck_db_role' => true/false
* - 'admin_strict_reference' => int|null (ex: 88888888)
*/
function require_login_or_redirect(?array $allowedRoles = null, array $options = []): void {
if (!is_logged_in()) {
function require_login_or_redirect(?array $allowedRoles = null, array $options = []): void
{
if (! is_logged_in()) {
redirect_to_login_with_next($_SERVER['REQUEST_URI'] ?? '/');
}
enforce_session_timeout(7200);
$pdo = $options['pdo'] ?? ($GLOBALS['pdo'] ?? null);
$recheck = (bool)($options['recheck_db_role'] ?? false);
$pdo = $options['pdo'] ?? ($GLOBALS['pdo'] ?? null);
$recheck = (bool) ($options['recheck_db_role'] ?? false);
// Rôle: session d'abord, DB si demandé (ou si role manquant)
$userRole = strtolower((string)($_SESSION['role'] ?? ''));
$userRole = strtolower((string) ($_SESSION['role'] ?? ''));
if (($userRole === '' || $recheck) && ($pdo instanceof PDO)) {
$userRole = refresh_role_from_db($pdo);
}
if ($allowedRoles) {
$allowed = array_map(static fn($r) => strtolower((string)$r), $allowedRoles);
$allowed = array_map(static fn($r) => strtolower((string) $r), $allowedRoles);
if (!in_array($userRole, $allowed, true)) {
if (! in_array($userRole, $allowed, true)) {
redirect_forbidden($_SERVER['REQUEST_URI'] ?? '/');
}
@@ -120,8 +142,8 @@ function require_login_or_redirect(?array $allowedRoles = null, array $options =
if (in_array('admin', $allowed, true)) {
$strictRef = $options['admin_strict_reference'] ?? null;
if ($strictRef !== null) {
$refId = (int)($_SESSION['reference_id'] ?? 0);
if ($refId !== (int)$strictRef) {
$refId = (int) ($_SESSION['reference_id'] ?? 0);
if ($refId !== (int) $strictRef) {
redirect_forbidden($_SERVER['REQUEST_URI'] ?? '/');
}
}
@@ -134,35 +156,40 @@ function require_login_or_redirect(?array $allowedRoles = null, array $options =
========================= */
/** Token CSRF session */
function ensure_csrf_token(): string {
function ensure_csrf_token(): string
{
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return (string)$_SESSION['csrf_token'];
return (string) $_SESSION['csrf_token'];
}
/** Exige POST + CSRF (supporte aussi header X-CSRF-Token si tu veux fetch/AJAX) */
function require_post_csrf(): void {
function require_post_csrf(): void
{
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
http_response_code(405);
exit('Méthode non autorisée');
}
$token = $_POST['csrf_token'] ?? '';
if ($token === '' && !empty($_SERVER['HTTP_X_CSRF_TOKEN'])) {
$token = (string)$_SERVER['HTTP_X_CSRF_TOKEN'];
if ($token === '' && ! empty($_SERVER['HTTP_X_CSRF_TOKEN'])) {
$token = (string) $_SERVER['HTTP_X_CSRF_TOKEN'];
}
if ($token === '' || !hash_equals((string)($_SESSION['csrf_token'] ?? ''), (string)$token)) {
if ($token === '' || ! hash_equals((string) ($_SESSION['csrf_token'] ?? ''), (string) $token)) {
http_response_code(419);
exit('CSRF invalide');
}
}
/** Log admin minimal */
function log_admin_action(PDO $pdo, int $userId, string $action, string $details = ''): void {
function log_admin_action(PDO $pdo, int $userId, string $action, string $details = ''): void
{
// limite simple pour éviter d'exploser la table
if (strlen($details) > 2000) $details = substr($details, 0, 2000);
if (strlen($details) > 2000) {
$details = substr($details, 0, 2000);
}
$pdo->prepare(
"INSERT INTO admin_actions_log (user_id, action, details, ip) VALUES (?,?,?,?)"
@@ -170,6 +197,6 @@ function log_admin_action(PDO $pdo, int $userId, string $action, string $details
$userId,
$action,
$details,
$_SERVER['REMOTE_ADDR'] ?? ''
$_SERVER['REMOTE_ADDR'] ?? '',
]);
}
+1758 -1593
View File
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+1767 -1601
View File
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+55 -40
View File
@@ -1,37 +1,41 @@
<?php
require_once __DIR__ . '/../includes/init_praticien.php';
require_once __DIR__ . '/../includes/init_praticien.php';
$success = $erreur = '';
$id = $_SESSION['compte_id'];
$success = $erreur = '';
$id = $_SESSION['compte_id'];
// Récupérer e-mail actuel
$stmt = $pdo->prepare("SELECT email FROM compte WHERE id = ?");
$stmt->execute([$id]);
$compte = $stmt->fetch();
// Récupérer e-mail actuel
$stmt = $pdo->prepare("SELECT email, mot_de_passe FROM compte WHERE id = ?");
$stmt->execute([$id]);
$compte = $stmt->fetch();
if (!$compte) {
if (! $compte) {
die($translations['compte_introuvable']);
}
}
$email_actuel = $compte['email'];
$email_actuel = $compte['email'];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
require_post_csrf();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$nouvel_email = trim($_POST['email'] ?? '');
$mdp1 = $_POST['mot_de_passe'] ?? '';
$mdp2 = $_POST['confirm_mot_de_passe'] ?? '';
$mdpActuel = $_POST['mot_de_passe_actuel'] ?? '';
$mdp1 = $_POST['mot_de_passe'] ?? '';
$mdp2 = $_POST['confirm_mot_de_passe'] ?? '';
if (!filter_var($nouvel_email, FILTER_VALIDATE_EMAIL)) {
if (! password_verify($mdpActuel, (string) ($compte['mot_de_passe'] ?? ''))) {
$erreur = $translations['erreur_mdp_actuel_incorrect'] ?? 'Mot de passe actuel incorrect.';
} elseif (! filter_var($nouvel_email, FILTER_VALIDATE_EMAIL)) {
$erreur = $translations['erreur_email_non_valide'];
} elseif ($mdp1 && ($mdp1 !== $mdp2 || strlen($mdp1) < 8)) {
$erreur = $translations['erreur_mdp_invalides'];
} else {
// Vérifier si l'email est déjà pris (sauf si c’est le sien)
$stmt = $pdo->prepare("SELECT id FROM compte WHERE email = ? AND id != ?");
$stmt->execute([$nouvel_email, $id]);
if ($stmt->fetch()) {
$erreur = $translations['err_email_deja_utilise'];
} else {
// Mise à jour
if ($mdp1) {
$hash = password_hash($mdp1, PASSWORD_DEFAULT);
$stmt = $pdo->prepare("UPDATE compte SET email = ?, mot_de_passe = ? WHERE id = ?");
@@ -40,37 +44,48 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$stmt = $pdo->prepare("UPDATE compte SET email = ? WHERE id = ?");
$stmt->execute([$nouvel_email, $id]);
}
$success = $translations['success_compte_mis_a_jour'];
// Optionnel : mettre à jour $email_actuel pour refléter l'écran
$success = $translations['success_compte_mis_a_jour'];
$email_actuel = $nouvel_email;
// garder la valeur en mémoire pour la suite de la page
$compte['mot_de_passe'] = $mdp1 ? $hash : $compte['mot_de_passe'];
}
}
}
require_once '../includes/header.php';
}
require_once '../includes/header.php';
?>
<div class="main1">
<div class="container py-5">
<form method="POST" class="shadow p-4 rounded bg-light">
<h2 class="text-center mb-4"><?= htmlspecialchars($translations['h2_infos_connexion']) ?></h2>
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(ensure_csrf_token(), ENT_QUOTES, 'UTF-8') ?>">
<h2 class="text-center mb-4"><?php echo htmlspecialchars($translations['h2_infos_connexion']) ?></h2>
<?php if (!empty($erreur)): ?>
<div class="alert alert-danger"><?= htmlspecialchars($erreur) ?></div>
<?php if (! empty($erreur)): ?>
<div class="alert alert-danger"><?php echo htmlspecialchars($erreur) ?></div>
<?php endif; ?>
<?php if (!empty($success)): ?>
<div class="alert alert-success"><?= htmlspecialchars($success) ?></div>
<?php if (! empty($success)): ?>
<div class="alert alert-success"><?php echo htmlspecialchars($success) ?></div>
<?php endif; ?>
<div class="mb-3">
<label class="form-label"><?= htmlspecialchars($translations['label_email']) ?></label>
<input type="email" name="email" class="form-control" value="<?= htmlspecialchars($email_actuel ?? '') ?>" required>
<label class="form-label"><?php echo htmlspecialchars($translations['label_email']) ?></label>
<input type="email" name="email" class="form-control" value="<?php echo htmlspecialchars($email_actuel ?? '') ?>" required>
</div>
<div class="mb-3">
<label class="form-label"><?php echo htmlspecialchars($translations['label_mdp_actuel'] ?? 'Mot de passe actuel') ?></label>
<div class="field-wrapper">
<input type="password" name="mot_de_passe_actuel" id="mot_de_passe_actuel" class="form-control"
placeholder="<?php echo htmlspecialchars($translations['placeholder_mdp_actuel'] ?? 'Entrez votre mot de passe actuel') ?>" required>
<span class="toggle-password" onclick="toggle('mot_de_passe_actuel')">👁️</span>
</div>
</div>
<div class="mb-3">
<label class="form-label"><?= htmlspecialchars($translations['label_nouveau_mdp_opt']) ?></label>
<label class="form-label"><?php echo htmlspecialchars($translations['label_nouveau_mdp_opt']) ?></label>
<div class="field-wrapper">
<input type="password" name="mot_de_passe" id="mot_de_passe" class="form-control"
placeholder="<?= htmlspecialchars($translations['placeholder_nouveau_mdp']) ?>">
placeholder="<?php echo htmlspecialchars($translations['placeholder_nouveau_mdp']) ?>">
<span class="toggle-password" onclick="toggle('mot_de_passe')">👁️</span>
</div>
<div class="strength-bar mt-2">
@@ -79,21 +94,21 @@ require_once '../includes/header.php';
<div class="strength-fill"></div>
</div>
<div id="niveau-securite" class="mb-2 text-muted">
<?= htmlspecialchars($translations['securite_label']) ?> : <?= htmlspecialchars($translations['securite_vide']) ?>
<?php echo htmlspecialchars($translations['securite_label']) ?> : <?php echo htmlspecialchars($translations['securite_vide']) ?>
</div>
</div>
<div class="mb-3">
<label class="form-label"><?= htmlspecialchars($translations['label_confirmer_mdp']) ?></label>
<label class="form-label"><?php echo htmlspecialchars($translations['label_confirmer_mdp']) ?></label>
<div class="field-wrapper">
<input type="password" name="confirm_mot_de_passe" id="confirm_mot_de_passe" class="form-control"
placeholder="<?= htmlspecialchars($translations['placeholder_confirmer_mdp']) ?>">
placeholder="<?php echo htmlspecialchars($translations['placeholder_confirmer_mdp']) ?>">
<span class="toggle-password" onclick="toggle('confirm_mot_de_passe')">👁️</span>
</div>
</div>
<div class="mt-4 text-center">
<button type="submit" class="btn btn-primary">
<?= htmlspecialchars($translations['btn_mettre_a_jour']) ?>
<?php echo htmlspecialchars($translations['btn_mettre_a_jour']) ?>
</button>
</div>
</div>
@@ -112,11 +127,11 @@ document.getElementById('mot_de_passe').addEventListener('input', function () {
segments.forEach(s => s.style.backgroundColor = '#ccc');
// base label
const label = <?= json_encode($translations['securite_label'], JSON_UNESCAPED_UNICODE) ?>;
const faible = <?= json_encode($translations['securite_faible'], JSON_UNESCAPED_UNICODE) ?>;
const moyen = <?= json_encode($translations['securite_moyen'], JSON_UNESCAPED_UNICODE) ?>;
const fort = <?= json_encode($translations['securite_fort'], JSON_UNESCAPED_UNICODE) ?>;
const vide = <?= json_encode($translations['securite_vide'], JSON_UNESCAPED_UNICODE) ?>;
const label = <?php echo json_encode($translations['securite_label'], JSON_UNESCAPED_UNICODE) ?>;
const faible = <?php echo json_encode($translations['securite_faible'], JSON_UNESCAPED_UNICODE) ?>;
const moyen = <?php echo json_encode($translations['securite_moyen'], JSON_UNESCAPED_UNICODE) ?>;
const fort = <?php echo json_encode($translations['securite_fort'], JSON_UNESCAPED_UNICODE) ?>;
const vide = <?php echo json_encode($translations['securite_vide'], JSON_UNESCAPED_UNICODE) ?>;
niveauText.textContent = `${label} : ${vide}`;
niveauText.classList.remove("text-danger", "text-warning", "text-success");
@@ -152,7 +167,7 @@ document.getElementById('mot_de_passe').addEventListener('input', function () {
</div>
<footer class="site-footer">
<div class="container">
<p><?= $translations['copyr'] ?></p>
<p><?php echo $translations['copyr'] ?></p>
</div>
</footer>
</body>
+208 -150
View File
@@ -1,23 +1,25 @@
<?php
// /public/praticien/disponibilites.php
declare (strict_types = 1);
require_once __DIR__ . '/../includes/init_praticien.php';
$praticien_id = (int) ($_SESSION['reference_id'] ?? 0);
if ($praticien_id <= 0) {http_response_code(403);exit('Forbidden');}
if ($praticien_id <= 0) {
http_response_code(403);
exit('Forbidden');
}
// Durée RDV praticien (garde-fou strict)
// Durée RDV praticien
$stmt = $pdo->prepare("SELECT duree_rdv FROM praticien WHERE id = ?");
$stmt->execute([$praticien_id]);
$duree_minutes = (int) ($stmt->fetchColumn() ?: 0);
if ($duree_minutes <= 0) {
$duree_minutes = 30;
}
// fallback sécurité
// ---- Ajouter un créneau (POST) ----
$erreur = null;
$toMin = function (string $hhmm): ?int {
// input type="time" renvoie souvent HH:MM
if (! preg_match('/^\d{2}:\d{2}$/', $hhmm)) {
return null;
}
@@ -30,7 +32,14 @@
return $h * 60 + $m;
};
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['jour'], $_POST['heure_debut'], $_POST['heure_fin'])) {
// ---- Ajouter un créneau (POST) ----
if (
$_SERVER['REQUEST_METHOD'] === 'POST'
&& ($_POST['action'] ?? '') === 'add'
&& isset($_POST['jour'], $_POST['heure_debut'], $_POST['heure_fin'])
) {
require_post_csrf();
$jour = trim((string) $_POST['jour']);
$debut = trim((string) $_POST['heure_debut']);
$fin = trim((string) $_POST['heure_fin']);
@@ -43,7 +52,7 @@
} elseif ($md >= $mf) {
$erreur = $translations['erreur_debut_avant_fin'] ?? "Heure de début après l’heure de fin.";
} else {
$window = $mf - $md; // minutes
$window = $mf - $md;
if ($window < $duree_minutes) {
$erreur = sprintf(
@@ -69,194 +78,238 @@
}
}
// ---- Supprimer un créneau (GET) ----
// (idéalement: POST + CSRF, on garde GET pour aller vite)
if (isset($_GET['supprimer'])) {
$id = (int) $_GET['supprimer'];
$stmt = $pdo->prepare("DELETE FROM disponibilite WHERE id = ? AND praticien_id = ?");
$stmt->execute([$id, $praticien_id]);
header('Location: /public/praticien/disponibilites.php?ok=deleted', true, 303);
exit;
}
// ---- Récupérer les créneaux ----
$stmt = $pdo->prepare(
"SELECT * FROM disponibilite
"SELECT *
FROM disponibilite
WHERE praticien_id = ?
ORDER BY FIELD(jour_semaine,'lundi','mardi','mercredi','jeudi','vendredi','samedi','dimanche'), heure_debut"
);
$stmt->execute([$praticien_id]);
$dispos = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Prépare les traductions de jours
// Traductions jours
$jourKeyMap = [
'lundi' => 'jour_lundi', 'mardi' => 'jour_mardi', 'mercredi' => 'jour_mercredi',
'jeudi' => 'jour_jeudi', 'vendredi' => 'jour_vendredi', 'samedi' => 'jour_samedi', 'dimanche' => 'jour_dimanche',
'lundi' => 'jour_lundi',
'mardi' => 'jour_mardi',
'mercredi' => 'jour_mercredi',
'jeudi' => 'jour_jeudi',
'vendredi' => 'jour_vendredi',
'samedi' => 'jour_samedi',
'dimanche' => 'jour_dimanche',
];
// 1️⃣ Titre de la page
$PAGE_TITLE = $translations['titre_disponibilites'] ?? 'Disponibilités';
// 2️⃣ Ce qui doit aller dans <head> EN PLUS du header global
$PAGE_TITLE = $translations['titre_disponibilites'] ?? 'Disponibilités';
$HEAD_EXTRA = <<<HTML
<script src="/js/sweetalert2@11.js"></script>
HTML;
require_once __DIR__ . '/../includes/header.php';
?>
<div class="main1">
<div class="container py-5">
<div class="shadow p-4 rounded bg-light">
<h2 class="mb-4 text-center"><?php echo htmlspecialchars($translations['h2_disponibilites'] ?? 'Mes disponibilités') ?></h2>
<div class="container py-5">
<div class="shadow p-4 rounded bg-light">
<h2 class="mb-4 text-center"><?php echo htmlspecialchars($translations['h2_disponibilites'] ?? 'Mes disponibilités'); ?></h2>
<!-- Plus d'alertes Bootstrap ici : on affiche via Swal selon ?ok=... et $erreur -->
<div class="text-end">
<a href="/public/praticien/disponibilites_date.php" class="btn btn-outline-secondary mb-4">
<?php echo htmlspecialchars($translations['btn_ajouter_par_date'] ?? 'Ajouter par date') ?>
</a>
</div>
<div class="table-responsive mb-4">
<div class="text-end">
<a href="/public/praticien/disponibilites_date.php" class="btn btn-outline-secondary mb-4">
<?php echo htmlspecialchars($translations['btn_ajouter_par_date'] ?? 'Ajouter par date'); ?>
</a>
</div>
<table class="table table-bordered table-striped align-middle text-center mobile-cards" id="table_rdv">
<thead class="table-light">
<tr>
<th><?php echo htmlspecialchars($translations['table_jour'] ?? 'Jour') ?></th>
<th><?php echo htmlspecialchars($translations['table_debut'] ?? 'Début') ?></th>
<th><?php echo htmlspecialchars($translations['table_fin'] ?? 'Fin') ?></th>
<th><?php echo htmlspecialchars($translations['table_action'] ?? 'Action') ?></th>
</tr>
</thead>
<tbody>
<?php if ($dispos): foreach ($dispos as $d): ?>
<tr>
<td>
<?php
$key = $jourKeyMap[$d['jour_semaine']] ?? null;
echo htmlspecialchars($key ? ($translations[$key] ?? ucfirst($d['jour_semaine'])) : ucfirst($d['jour_semaine']));
?>
</td>
<td><?php echo htmlspecialchars(substr((string) $d['heure_debut'], 0, 5)) ?></td>
<td><?php echo htmlspecialchars(substr((string) $d['heure_fin'], 0, 5)) ?></td>
<td>
<button type="button"
<div class="table-responsive mb-4">
<table class="table table-bordered table-striped align-middle text-center mobile-cards" id="table_rdv">
<thead class="table-light">
<tr>
<th><?php echo htmlspecialchars($translations['table_jour'] ?? 'Jour'); ?></th>
<th><?php echo htmlspecialchars($translations['table_debut'] ?? 'Début'); ?></th>
<th><?php echo htmlspecialchars($translations['table_fin'] ?? 'Fin'); ?></th>
<th><?php echo htmlspecialchars($translations['table_action'] ?? 'Action'); ?></th>
</tr>
</thead>
<tbody>
<?php if ($dispos): ?>
<?php foreach ($dispos as $d): ?>
<tr>
<td>
<?php
$key = $jourKeyMap[$d['jour_semaine']] ?? null;
echo htmlspecialchars($key ? ($translations[$key] ?? ucfirst($d['jour_semaine'])) : ucfirst($d['jour_semaine']));
?>
</td>
<td><?php echo htmlspecialchars(substr((string) $d['heure_debut'], 0, 5)); ?></td>
<td><?php echo htmlspecialchars(substr((string) $d['heure_fin'], 0, 5)); ?></td>
<td>
<button
type="button"
class="btn btn-sm btn-danger js-del"
data-id="<?php echo (int) $d['id'] ?>">
<?php echo htmlspecialchars($translations['btn_supprimer'] ?? 'Supprimer') ?>
</button>
</td>
</tr>
<?php endforeach;else: ?>
<tr><td colspan="4"><?php echo htmlspecialchars($translations['aucun_creneau'] ?? 'Aucun créneau.') ?></td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
data-id="<?php echo (int) $d['id']; ?>">
<?php echo htmlspecialchars($translations['btn_supprimer'] ?? 'Supprimer'); ?>
</button>
</td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="4"><?php echo htmlspecialchars($translations['aucun_creneau'] ?? 'Aucun créneau.'); ?></td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<div class="card shadow-sm p-4">
<h4 class="mb-3 text-center"><?php echo htmlspecialchars($translations['card_ajouter_creneau'] ?? 'Ajouter un créneau') ?></h4>
<form method="POST">
<div class="mb-3">
<label class="form-label" for="jour"><?php echo htmlspecialchars($translations['label_jour'] ?? 'Jour') ?></label>
<select name="jour" id="jour" class="form-select" required>
<option value=""><?php echo htmlspecialchars($translations['option_choisir'] ?? '— Choisir —') ?></option>
<?php
$jours = [
'lundi' => $translations['jour_lundi'] ?? 'Lundi',
'mardi' => $translations['jour_mardi'] ?? 'Mardi',
'mercredi' => $translations['jour_mercredi'] ?? 'Mercredi',
'jeudi' => $translations['jour_jeudi'] ?? 'Jeudi',
'vendredi' => $translations['jour_vendredi'] ?? 'Vendredi',
'samedi' => $translations['jour_samedi'] ?? 'Samedi',
'dimanche' => $translations['jour_dimanche'] ?? 'Dimanche',
];
foreach ($jours as $val => $label):
?>
<option value="<?php echo htmlspecialchars($val) ?>"><?php echo htmlspecialchars($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="card shadow-sm p-4">
<h4 class="mb-3 text-center"><?php echo htmlspecialchars($translations['card_ajouter_creneau'] ?? 'Ajouter un créneau'); ?></h4>
<div class="mb-3">
<label class="form-label" for="heure_debut"><?php echo htmlspecialchars($translations['label_heure_debut'] ?? 'Heure de début') ?></label>
<input type="time" name="heure_debut" id="heure_debut" class="form-control" required>
</div>
<form method="POST">
<input type="hidden" name="action" value="add">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(ensure_csrf_token(), ENT_QUOTES, 'UTF-8'); ?>">
<div class="mb-3">
<label class="form-label" for="heure_fin"><?php echo htmlspecialchars($translations['label_heure_fin'] ?? 'Heure de fin') ?></label>
<input type="time" name="heure_fin" id="heure_fin" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label" for="jour"><?php echo htmlspecialchars($translations['label_jour'] ?? 'Jour'); ?></label>
<select name="jour" id="jour" class="form-select" required>
<option value=""><?php echo htmlspecialchars($translations['option_choisir'] ?? '— Choisir —'); ?></option>
<?php
$jours = [
'lundi' => $translations['jour_lundi'] ?? 'Lundi',
'mardi' => $translations['jour_mardi'] ?? 'Mardi',
'mercredi' => $translations['jour_mercredi'] ?? 'Mercredi',
'jeudi' => $translations['jour_jeudi'] ?? 'Jeudi',
'vendredi' => $translations['jour_vendredi'] ?? 'Vendredi',
'samedi' => $translations['jour_samedi'] ?? 'Samedi',
'dimanche' => $translations['jour_dimanche'] ?? 'Dimanche',
];
foreach ($jours as $val => $label):
?>
<option value="<?php echo htmlspecialchars($val); ?>"><?php echo htmlspecialchars($label); ?></option>
<?php endforeach; ?>
</select>
</div>
<button type="submit" class="btn btn-primary w-100">
<?php echo htmlspecialchars($translations['btn_ajouter'] ?? 'Ajouter') ?>
</button>
</form>
<div class="mb-3">
<label class="form-label" for="heure_debut"><?php echo htmlspecialchars($translations['label_heure_debut'] ?? 'Heure de début'); ?></label>
<input type="time" name="heure_debut" id="heure_debut" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label" for="heure_fin"><?php echo htmlspecialchars($translations['label_heure_fin'] ?? 'Heure de fin'); ?></label>
<input type="time" name="heure_fin" id="heure_fin" class="form-control" required>
</div>
<button type="submit" class="btn btn-primary w-100">
<?php echo htmlspecialchars($translations['btn_ajouter'] ?? 'Ajouter'); ?>
</button>
</form>
</div>
</div>
</div>
</div>
</div>
<script>
// --- SweetAlert2: messages succès / erreur depuis PHP ---
document.addEventListener('DOMContentLoaded', function(){
const okParam = '<?php echo isset($_GET['ok']) ? addslashes((string) $_GET['ok']) : '' ?>';
const erreurMsg = '<?php echo $erreur ? addslashes((string) $erreur) : '' ?>';
const csrfToken = <?php echo json_encode(ensure_csrf_token(), JSON_UNESCAPED_UNICODE); ?>;
document.addEventListener('DOMContentLoaded', function () {
const okParam = <?php echo json_encode(isset($_GET['ok']) ? (string) $_GET['ok'] : '', JSON_UNESCAPED_UNICODE); ?>;
const erreurMsg = <?php echo json_encode($erreur ?: '', JSON_UNESCAPED_UNICODE); ?>;
if (okParam === 'added') {
Swal.fire({
icon: 'success',
title: '<?php echo addslashes($translations["msg_creneau_ajoute"] ?? "Créneau ajouté.") ?>',
timer: 1800,
showConfirmButton: false
});
} else if (okParam === 'deleted') {
Swal.fire({
icon: 'success',
title: '<?php echo addslashes($translations["msg_creneau_supprime"] ?? "Créneau supprimé.") ?>',
timer: 1800,
showConfirmButton: false
});
Swal.fire({
icon: 'success',
title: <?php echo json_encode($translations['msg_creneau_ajoute'] ?? 'Créneau ajouté.', JSON_UNESCAPED_UNICODE); ?>,
timer: 1800,
showConfirmButton: false
});
if (window.history.replaceState) {
const cleanUrl = window.location.pathname;
window.history.replaceState({}, document.title, cleanUrl);
}
}
if (erreurMsg) {
Swal.fire({
icon: 'error',
title: '<?php echo addslashes($translations["erreur_titre"] ?? "Erreur") ?>',
html: erreurMsg,
showCancelButton: true,
confirmButtonText: '<?php echo addslashes($translations["btn_modifier_profil"] ?? "Modifier mon profil") ?>',
cancelButtonText: 'OK'
}).then((result) => {
if (result.isConfirmed) {
window.location.href = '/public/praticien/modifier.php';
}
});
}
Swal.fire({
icon: 'error',
title: <?php echo json_encode($translations['erreur_titre'] ?? 'Erreur', JSON_UNESCAPED_UNICODE); ?>,
html: erreurMsg,
showCancelButton: true,
confirmButtonText: <?php echo json_encode($translations['btn_modifier_profil'] ?? 'Modifier mon profil', JSON_UNESCAPED_UNICODE); ?>,
cancelButtonText: 'OK'
}).then((result) => {
if (result.isConfirmed) {
window.location.href = '/public/praticien/modifier.php';
}
});
}
});
// --- Confirmation de suppression avec SweetAlert2 ---
document.addEventListener('click', async function(e){
document.addEventListener('click', async function (e) {
const btn = e.target.closest('.js-del');
if (!btn) return;
const id = btn.dataset.id;
if (!id) return;
const result = await Swal.fire({
icon: 'warning',
title: '<?php echo addslashes($translations["confirm_title"] ?? "Confirmation") ?>',
text: '<?php echo addslashes($translations["confirm_supprimer_creneau"] ?? "Supprimer ce créneau ?") ?>',
title: <?php echo json_encode($translations['confirm_title'] ?? 'Confirmation', JSON_UNESCAPED_UNICODE); ?>,
text: <?php echo json_encode($translations['confirm_supprimer_creneau'] ?? 'Supprimer ce créneau ?', JSON_UNESCAPED_UNICODE); ?>,
showCancelButton: true,
confirmButtonText: '<?php echo addslashes($translations["btn_da_udalit"] ?? "Oui") ?>',
cancelButtonText: '<?php echo addslashes($translations["btn_annuler"] ?? "Annuler") ?>',
confirmButtonText: <?php echo json_encode($translations['btn_da_udalit'] ?? 'Oui', JSON_UNESCAPED_UNICODE); ?>,
cancelButtonText: <?php echo json_encode($translations['btn_annuler'] ?? 'Annuler', JSON_UNESCAPED_UNICODE); ?>,
reverseButtons: true
});
if (result.isConfirmed) {
// Suppression côté serveur (GET) — garde la mécanique existante
window.location.href = '?supprimer=' + encodeURIComponent(id);
}
if (!result.isConfirmed) return;
const formData = new FormData();
formData.append('id', String(id));
formData.append('csrf_token', csrfToken);
fetch('/public/praticien/supprimer_disponibilite.php', {
method: 'POST',
headers: {
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: formData
})
.then(async r => {
const text = await r.text();
try {
return JSON.parse(text);
} catch (e) {
return { ok: false, message: text };
}
})
.then(data => {
if (data.ok) {
Swal.fire({
icon: 'success',
title: <?php echo json_encode($translations['msg_creneau_supprime'] ?? 'Créneau supprimé.', JSON_UNESCAPED_UNICODE); ?>,
timer: 1400,
showConfirmButton: false
}).then(() => {
window.location.reload();
});
} else {
Swal.fire({
icon: 'error',
title: <?php echo json_encode($translations['erreur_titre'] ?? 'Erreur', JSON_UNESCAPED_UNICODE); ?>,
text: data.message || 'Suppression impossible.'
});
}
})
.catch(() => {
Swal.fire({
icon: 'error',
title: 'Erreur réseau',
text: 'Veuillez réessayer.'
});
});
});
</script>
<!--*********Adaptation des tables pour un affichage mobile***************-->
<script defer>
(function(){
function initMobileCards(tbl){
@@ -269,15 +322,20 @@ document.addEventListener('click', async function(e){
});
document.documentElement.classList.add('mc-ready');
}
function ready(fn){ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fn); else fn(); }
ready(function(){ initMobileCards(document.getElementById('table_rdv')); });
function ready(fn){
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fn);
else fn();
}
ready(function(){
initMobileCards(document.getElementById('table_rdv'));
});
})();
</script>
<footer class="site-footer">
<div class="container">
<p><?php echo $translations['copyr'] ?? '&copy; ' . date('Y') ?></p>
<p><?php echo $translations['copyr'] ?? '&copy; ' . date('Y'); ?></p>
</div>
</footer>
</body>
</html>
</html>
+192 -122
View File
@@ -1,17 +1,23 @@
<?php
declare (strict_types = 1);
// /public/praticien/disponibilites_date.php
require_once __DIR__ . '/../includes/init_praticien.php';
$praticien_id = (int) ($_SESSION['reference_id'] ?? 0);
if ($praticien_id <= 0) {http_response_code(403);exit('Forbidden');}
// Durée RDV praticien (garde-fou strict)
$praticien_id = (int) ($_SESSION['reference_id'] ?? 0);
if ($praticien_id <= 0) {
http_response_code(403);
exit('Forbidden');
}
// Durée RDV praticien
$stmt = $pdo->prepare("SELECT duree_rdv FROM praticien WHERE id = ?");
$stmt->execute([$praticien_id]);
$duree_minutes = (int) ($stmt->fetchColumn() ?: 0);
if ($duree_minutes <= 0) {
$duree_minutes = 30;
}
// ---- Ajouter une dispo ponctuelle (POST) ----
$erreur = null;
$toMin = function (string $hhmm): ?int {
@@ -27,13 +33,20 @@
return $h * 60 + $m;
};
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['date_dispo'], $_POST['heure_debut'], $_POST['heure_fin'])) {
// ---- Ajouter une dispo ponctuelle (POST + CSRF)
if (
$_SERVER['REQUEST_METHOD'] === 'POST'
&& ($_POST['action'] ?? '') === 'add'
&& isset($_POST['date_dispo'], $_POST['heure_debut'], $_POST['heure_fin'])
) {
require_post_csrf();
$date_dispo = trim((string) $_POST['date_dispo']); // YYYY-MM-DD
$debut = trim((string) $_POST['heure_debut']); // HH:MM
$fin = trim((string) $_POST['heure_fin']); // HH:MM
// Validation date simple
$dt = DateTimeImmutable::createFromFormat('Y-m-d', $date_dispo);
if (! $dt || $dt->format('Y-m-d') !== $date_dispo) {
$erreur = $translations['erreur_format_date'] ?? "Format de date invalide.";
} else {
@@ -59,10 +72,10 @@
$duree_minutes
);
} else {
$stmt = $pdo->prepare(
"INSERT INTO disponibilite_date (praticien_id, date_dispo, heure_debut, heure_fin)
VALUES (?, ?, ?, ?)"
);
$stmt = $pdo->prepare("
INSERT INTO disponibilite_date (praticien_id, date_dispo, heure_debut, heure_fin)
VALUES (?, ?, ?, ?)
");
$stmt->execute([$praticien_id, $date_dispo, $debut, $fin]);
header('Location: /public/praticien/disponibilites_date.php?ok=added', true, 303);
@@ -72,158 +85,209 @@
}
}
// ---- Supprimer une plage datée (GET) ----
if (isset($_GET['supprimer'])) {
$id = (int) $_GET['supprimer'];
$stmt = $pdo->prepare("DELETE FROM disponibilite_date WHERE id = ? AND praticien_id = ?");
$stmt->execute([$id, $praticien_id]);
header('Location: /public/praticien/disponibilites_date.php?ok=deleted', true, 303);
exit;
}
// ---- Récupérer les plages datées ----
$stmt = $pdo->prepare(
"SELECT id, date_dispo, heure_debut, heure_fin
FROM disponibilite_date
WHERE praticien_id = ?
ORDER BY date_dispo ASC, heure_debut ASC"
);
// ---- Récupérer les plages datées
$stmt = $pdo->prepare("
SELECT id, date_dispo, heure_debut, heure_fin
FROM disponibilite_date
WHERE praticien_id = ?
ORDER BY date_dispo ASC, heure_debut ASC
");
$stmt->execute([$praticien_id]);
$dates = $stmt->fetchAll(PDO::FETCH_ASSOC);
// 1️⃣ Titre de la page
$PAGE_TITLE = $translations['titre_disponibilites_date'] ?? 'Disponibilités (dates)';
// 2️⃣ Ce qui doit aller dans <head> EN PLUS du header global
$PAGE_TITLE = $translations['titre_disponibilites_date'] ?? 'Disponibilités (dates)';
$HEAD_EXTRA = <<<HTML
<script src="/js/sweetalert2@11.js"></script>
HTML;
require_once __DIR__ . '/../includes/header.php';
?>
<div class="main1">
<div class="container py-5">
<div class="shadow p-4 rounded bg-light">
<h2 class="mb-4 text-center"><?php echo htmlspecialchars($translations['h2_disponibilites_date'] ?? 'Mes disponibilités ponctuelles') ?></h2>
<div class="container py-5">
<div class="shadow p-4 rounded bg-light">
<h2 class="mb-4 text-center">
<?php echo htmlspecialchars($translations['h2_disponibilites_date'] ?? 'Mes disponibilités ponctuelles'); ?>
</h2>
<div class="d-flex justify-content-between align-items-center mb-3">
<a class="btn btn-outline-secondary"
href="/public/praticien/disponibilites.php">
<?php echo htmlspecialchars($translations['retour_au_planning'] ?? 'Retour') ?>
</a>
</div>
<div class="d-flex justify-content-between align-items-center mb-3">
<a class="btn btn-outline-secondary" href="/public/praticien/disponibilites.php">
<?php echo htmlspecialchars($translations['retour_au_planning'] ?? 'Retour'); ?>
</a>
</div>
<div class="table-responsive mb-4">
<table class="table table-bordered table-striped align-middle text-center mobile-cards" id="table_rdv">
<thead class="table-light">
<tr>
<th><?php echo htmlspecialchars($translations['cl_date_rdv'] ?? 'Date') ?></th>
<th><?php echo htmlspecialchars($translations['table_debut'] ?? 'Début') ?></th>
<th><?php echo htmlspecialchars($translations['table_fin'] ?? 'Fin') ?></th>
<th><?php echo htmlspecialchars($translations['table_action'] ?? 'Action') ?></th>
</tr>
</thead>
<tbody>
<?php if ($dates): foreach ($dates as $d): ?>
<tr>
<td>
<?php echo htmlspecialchars(
date('d/m/Y', strtotime($d['date_dispo']))
) ?>
</td>
<td><?php echo htmlspecialchars(substr((string) $d['heure_debut'], 0, 5)) ?></td>
<td><?php echo htmlspecialchars(substr((string) $d['heure_fin'], 0, 5)) ?></td>
<td>
<button type="button"
<div class="table-responsive mb-4">
<table class="table table-bordered table-striped align-middle text-center mobile-cards" id="table_rdv">
<thead class="table-light">
<tr>
<th><?php echo htmlspecialchars($translations['cl_date_rdv'] ?? 'Date'); ?></th>
<th><?php echo htmlspecialchars($translations['table_debut'] ?? 'Début'); ?></th>
<th><?php echo htmlspecialchars($translations['table_fin'] ?? 'Fin'); ?></th>
<th><?php echo htmlspecialchars($translations['table_action'] ?? 'Action'); ?></th>
</tr>
</thead>
<tbody>
<?php if ($dates): ?>
<?php foreach ($dates as $d): ?>
<tr>
<td><?php echo htmlspecialchars(date('d/m/Y', strtotime((string) $d['date_dispo']))); ?></td>
<td><?php echo htmlspecialchars(substr((string) $d['heure_debut'], 0, 5)); ?></td>
<td><?php echo htmlspecialchars(substr((string) $d['heure_fin'], 0, 5)); ?></td>
<td>
<button
type="button"
class="btn btn-sm btn-danger js-del"
data-id="<?php echo (int) $d['id'] ?>">
<?php echo htmlspecialchars($translations['btn_supprimer'] ?? 'Supprimer') ?>
</button>
</td>
</tr>
<?php endforeach;else: ?>
<tr><td colspan="4"><?php echo htmlspecialchars($translations['aucun_creneau'] ?? 'Aucun créneau.') ?></td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
data-id="<?php echo (int) $d['id']; ?>">
<?php echo htmlspecialchars($translations['btn_supprimer'] ?? 'Supprimer'); ?>
</button>
</td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="4"><?php echo htmlspecialchars($translations['aucun_creneau'] ?? 'Aucun créneau.'); ?></td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<div class="card shadow-sm p-4">
<h4 class="mb-3 text-center"><?php echo htmlspecialchars($translations['card_ajouter_creneau_date'] ?? 'Ajouter une disponibilité ponctuelle') ?></h4>
<div class="card shadow-sm p-4">
<h4 class="mb-3 text-center">
<?php echo htmlspecialchars($translations['card_ajouter_creneau_date'] ?? 'Ajouter une disponibilité ponctuelle'); ?>
</h4>
<form method="POST">
<div class="mb-3">
<label class="form-label" for="date_dispo"><?php echo htmlspecialchars($translations['cl_date_rdv'] ?? 'Date') ?></label>
<input type="date" name="date_dispo" id="date_dispo" class="form-control" required>
</div>
<form method="POST">
<input type="hidden" name="action" value="add">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(ensure_csrf_token(), ENT_QUOTES, 'UTF-8'); ?>">
<div class="mb-3">
<label class="form-label" for="heure_debut"><?php echo htmlspecialchars($translations['label_heure_debut'] ?? 'Heure de début') ?></label>
<input type="time" name="heure_debut" id="heure_debut" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label" for="date_dispo"><?php echo htmlspecialchars($translations['cl_date_rdv'] ?? 'Date'); ?></label>
<input type="date" name="date_dispo" id="date_dispo" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label" for="heure_fin"><?php echo htmlspecialchars($translations['label_heure_fin'] ?? 'Heure de fin') ?></label>
<input type="time" name="heure_fin" id="heure_fin" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label" for="heure_debut"><?php echo htmlspecialchars($translations['label_heure_debut'] ?? 'Heure de début'); ?></label>
<input type="time" name="heure_debut" id="heure_debut" class="form-control" required>
</div>
<button type="submit" class="btn btn-primary w-100">
<?php echo htmlspecialchars($translations['btn_ajouter'] ?? 'Ajouter') ?>
</button>
</form>
<div class="mb-3">
<label class="form-label" for="heure_fin"><?php echo htmlspecialchars($translations['label_heure_fin'] ?? 'Heure de fin'); ?></label>
<input type="time" name="heure_fin" id="heure_fin" class="form-control" required>
</div>
<button type="submit" class="btn btn-primary w-100">
<?php echo htmlspecialchars($translations['btn_ajouter'] ?? 'Ajouter'); ?>
</button>
</form>
</div>
</div>
</div>
</div>
</div>
<script>
const csrfToken = <?php echo json_encode(ensure_csrf_token(), JSON_UNESCAPED_UNICODE); ?>;
document.addEventListener('DOMContentLoaded', function(){
const okParam = '<?php echo isset($_GET['ok']) ? addslashes((string) $_GET['ok']) : '' ?>';
const erreurMsg = '<?php echo $erreur ? addslashes((string) $erreur) : '' ?>';
const okParam = <?php echo json_encode(isset($_GET['ok']) ? (string) $_GET['ok'] : '', JSON_UNESCAPED_UNICODE); ?>;
const erreurMsg = <?php echo json_encode($erreur ?: '', JSON_UNESCAPED_UNICODE); ?>;
if (okParam === 'added') {
Swal.fire({ icon:'success', title:'<?php echo addslashes($translations["msg_creneau_ajoute"] ?? "Créneau ajouté.") ?>', timer:1800, showConfirmButton:false });
} else if (okParam === 'deleted') {
Swal.fire({ icon:'success', title:'<?php echo addslashes($translations["msg_creneau_supprime"] ?? "Créneau supprimé.") ?>', timer:1800, showConfirmButton:false });
Swal.fire({
icon: 'success',
title: <?php echo json_encode($translations["msg_creneau_ajoute"] ?? "Créneau ajouté.", JSON_UNESCAPED_UNICODE); ?>,
timer: 1800,
showConfirmButton: false
});
if (window.history.replaceState) {
window.history.replaceState({}, document.title, window.location.pathname);
}
}
if (erreurMsg) {
Swal.fire({
icon: 'error',
title: '<?php echo addslashes($translations["erreur_titre"] ?? "Erreur") ?>',
html: erreurMsg,
showCancelButton: true,
confirmButtonText: '<?php echo addslashes($translations["btn_modifier_profil"] ?? "Modifier mon profil") ?>',
cancelButtonText: 'OK'
}).then((result) => {
if (result.isConfirmed) {
window.location.href = '/public/praticien/modifier.php';
}
});
}
Swal.fire({
icon: 'error',
title: <?php echo json_encode($translations["erreur_titre"] ?? "Erreur", JSON_UNESCAPED_UNICODE); ?>,
html: erreurMsg,
showCancelButton: true,
confirmButtonText: <?php echo json_encode($translations["btn_modifier_profil"] ?? "Modifier mon profil", JSON_UNESCAPED_UNICODE); ?>,
cancelButtonText: 'OK'
}).then((result) => {
if (result.isConfirmed) {
window.location.href = '/public/praticien/modifier.php';
}
});
}
});
document.addEventListener('click', async function(e){
const btn = e.target.closest('.js-del');
if (!btn) return;
const id = btn.dataset.id;
if (!id) return;
const result = await Swal.fire({
icon: 'warning',
title: '<?php echo addslashes($translations["confirm_title"] ?? "Confirmation") ?>',
text: '<?php echo addslashes($translations["confirm_supprimer_creneau"] ?? "Supprimer ce créneau ?") ?>',
title: <?php echo json_encode($translations["confirm_title"] ?? "Confirmation", JSON_UNESCAPED_UNICODE); ?>,
text: <?php echo json_encode($translations["confirm_supprimer_creneau"] ?? "Supprimer ce créneau ?", JSON_UNESCAPED_UNICODE); ?>,
showCancelButton: true,
confirmButtonText: '<?php echo addslashes($translations["btn_da_udalit"] ?? "Oui") ?>',
cancelButtonText: '<?php echo addslashes($translations["btn_annuler"] ?? "Annuler") ?>',
confirmButtonText: <?php echo json_encode($translations["btn_da_udalit"] ?? "Oui", JSON_UNESCAPED_UNICODE); ?>,
cancelButtonText: <?php echo json_encode($translations["btn_annuler"] ?? "Annuler", JSON_UNESCAPED_UNICODE); ?>,
reverseButtons: true
});
if (result.isConfirmed) {
window.location.href = '?supprimer=' + encodeURIComponent(id);
}
if (!result.isConfirmed) return;
const formData = new FormData();
formData.append('id', String(id));
formData.append('csrf_token', csrfToken);
fetch('/public/praticien/supprimer_disponibilite_date.php', {
method: 'POST',
headers: {
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: formData
})
.then(async r => {
const text = await r.text();
try {
return JSON.parse(text);
} catch (e) {
return { ok: false, message: text };
}
})
.then(data => {
if (data.ok) {
Swal.fire({
icon: 'success',
title: <?php echo json_encode($translations["msg_creneau_supprime"] ?? "Créneau supprimé.", JSON_UNESCAPED_UNICODE); ?>,
timer: 1400,
showConfirmButton: false
}).then(() => {
window.location.reload();
});
} else {
Swal.fire({
icon: 'error',
title: <?php echo json_encode($translations["erreur_titre"] ?? "Erreur", JSON_UNESCAPED_UNICODE); ?>,
text: data.message || 'Suppression impossible.'
});
}
})
.catch(() => {
Swal.fire({
icon: 'error',
title: 'Erreur réseau',
text: 'Veuillez réessayer.'
});
});
});
</script>
<!--*********Adaptation des tables pour un affichage mobile***************-->
<script defer>
(function(){
function initMobileCards(tbl){
@@ -236,14 +300,20 @@ document.addEventListener('click', async function(e){
});
document.documentElement.classList.add('mc-ready');
}
function ready(fn){ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fn); else fn(); }
ready(function(){ initMobileCards(document.getElementById('table_rdv')); });
function ready(fn){
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fn);
else fn();
}
ready(function(){
initMobileCards(document.getElementById('table_rdv'));
});
})();
</script>
<footer class="site-footer">
<div class="container">
<p><?php echo $translations['copyr'] ?? '&copy; ' . date('Y') ?></p>
<p><?php echo $translations['copyr'] ?? '&copy; ' . date('Y'); ?></p>
</div>
</footer>
</body>
</html>
</html>
+70 -48
View File
@@ -1,86 +1,113 @@
<?php
require_once __DIR__ . '/../includes/init_praticien.php';
require_once __DIR__ . '/../includes/init_praticien.php';
$id = $_SESSION['reference_id'] ?? null;
if (! $id) {
http_response_code(403);
exit('Forbidden');
}
$currentLang = $lang ?? 'ru';
if (! in_array($currentLang, ['ru', 'fr', 'en'], true)) {
$currentLang = 'ru';
}
// Base praticien
$stmt = $pdo->prepare("SELECT * FROM praticien WHERE id = ?");
$stmt->execute([$id]);
$praticien = $stmt->fetch();
// Récupérer les infos du praticien
$stmt = $pdo->prepare("SELECT * FROM praticien WHERE id = ?");
$stmt->execute([$_SESSION['reference_id'] ?? 0]);
$praticien = $stmt->fetch();
if (! $praticien) {
http_response_code(404);
exit(htmlspecialchars($translations['praticien_introuvable'] ?? 'Introuvable.'));
}
$id = $_SESSION['reference_id'] ?? null;
if (!$id) { http_response_code(403); exit('Forbidden'); }
require_once __DIR__ . '/../includes/header.php';
// Traduction courante
$stmtTr = $pdo->prepare("
SELECT *
FROM praticien_traduction
WHERE praticien_id = ? AND langue = ?
LIMIT 1
");
$stmtTr->execute([$id, $currentLang]);
$traduction = $stmtTr->fetch() ?: null;
// Valeurs affichées avec fallback
$nomAffiche = $traduction['nom_affiche'] ?? $praticien['nom'] ?? '';
$prenomAffiche = $traduction['prenom_affiche'] ?? $praticien['prenom'] ?? '';
$villeAffichee = $traduction['ville_affichee'] ?? $praticien['ville'] ?? '';
$titreAffiche = $traduction['titre'] ?? $praticien['titre'] ?? '';
$specialiteAffiche = $traduction['specialite'] ?? $praticien['specialite'] ?? '';
$presentationAffiche = $traduction['presentation'] ?? $praticien['presentation'] ?? '';
$dispoAffiche = $traduction['disponibilite'] ?? $praticien['disponibilite'] ?? '';
require_once __DIR__ . '/../includes/header.php';
?>
<div class="main1">
<div class="container py-5">
<div class="shadow p-4 rounded bg-light">
<h2 class="mb-4 text-center"><?= htmlspecialchars($translations['mes_infos_heading']) ?></h2>
<h2 class="mb-4 text-center"><?php echo htmlspecialchars($translations['mes_infos_heading']) ?></h2>
<div class="card-body">
<div class="row g-4 mb-3">
<!-- Colonne gauche -->
<div class="col-12 col-sm-6">
<p><strong><?= htmlspecialchars($translations['label_nom']) ?> :</strong>
<?= htmlspecialchars($praticien['nom'] ?? '') ?></p>
<p><strong><?php echo htmlspecialchars($translations['label_nom']) ?> :</strong>
<?php echo htmlspecialchars($nomAffiche) ?></p>
<p><strong><?= htmlspecialchars($translations['label_prenom']) ?> :</strong>
<?= htmlspecialchars($praticien['prenom'] ?? '') ?></p>
<p><strong><?php echo htmlspecialchars($translations['label_prenom']) ?> :</strong>
<?php echo htmlspecialchars($prenomAffiche) ?></p>
<p><strong><?= htmlspecialchars($translations['label_email']) ?> :</strong>
<?= htmlspecialchars($praticien['email'] ?? '') ?></p>
<p><strong><?php echo htmlspecialchars($translations['label_email']) ?> :</strong>
<?php echo htmlspecialchars($praticien['email'] ?? '') ?></p>
<p><strong><?= htmlspecialchars($translations['label_telephone']) ?> :</strong>
<?= htmlspecialchars($praticien['telephone'] ?? '') ?></p>
<p><strong><?php echo htmlspecialchars($translations['label_telephone']) ?> :</strong>
<?php echo htmlspecialchars($praticien['telephone'] ?? '') ?></p>
<div>
<strong><?= htmlspecialchars($translations['alt_photo']) ?> :</strong><br><br>
<?php if (!empty($praticien['photo'])): ?>
<strong><?php echo htmlspecialchars($translations['alt_photo']) ?> :</strong><br><br>
<?php if (! empty($praticien['photo'])): ?>
<img
src="/public/uploads/<?= htmlspecialchars($praticien['photo']) ?>"
alt="<?= htmlspecialchars($translations['alt_photo']) ?>"
src="/public/uploads/<?php echo htmlspecialchars($praticien['photo']) ?>"
alt="<?php echo htmlspecialchars($translations['alt_photo']) ?>"
class="img-fluid rounded-4 shadow"
style="max-width:230px;height:auto;">
<?php else: ?>
<span class="text-muted"><?= htmlspecialchars($translations['aucune_photo'] ?? 'Aucune photo') ?></span>
<span class="text-muted"><?php echo htmlspecialchars($translations['aucune_photo'] ?? 'Aucune photo') ?></span>
<?php endif; ?>
</div>
</div>
<!-- Colonne droite -->
<div class="col-12 col-sm-6">
<p><strong><?= htmlspecialchars($translations['placeholder_siret']) ?> :</strong>
<?= htmlspecialchars($praticien['siret'] ?? '') ?></p>
<p><strong><?php echo htmlspecialchars($translations['placeholder_siret']) ?> :</strong>
<?php echo htmlspecialchars($praticien['siret'] ?? '') ?></p>
<p><strong><?= htmlspecialchars($translations['label_specialite']) ?> :</strong>
<?= htmlspecialchars($praticien['specialite'] ?? '') ?></p>
<p><strong><?php echo htmlspecialchars($translations['label_specialite']) ?> :</strong>
<?php echo htmlspecialchars($specialiteAffiche) ?></p>
<!-- Ajout tarif et moyen de paiement -->
<p><strong><?= htmlspecialchars($translations['tarif_consultation']) ?> :</strong>
<?= htmlspecialchars($praticien['titre'] ?? '') ?></p>
<p><strong><?php echo htmlspecialchars($translations['tarif_consultation']) ?> :</strong>
<?php echo htmlspecialchars($titreAffiche) ?></p>
<p><strong><?= htmlspecialchars($translations['moyens_payement']) ?> :</strong>
<?= htmlspecialchars($praticien['disponibilite'] ?? '') ?></p>
<p><strong><?php echo htmlspecialchars($translations['moyens_payement']) ?> :</strong>
<?php echo htmlspecialchars($dispoAffiche) ?></p>
<p><strong><?= htmlspecialchars($translations['label_ville']) ?> :</strong>
<?= htmlspecialchars($praticien['ville'] ?? '') ?></p>
<p><strong><?php echo htmlspecialchars($translations['label_ville']) ?> :</strong>
<?php echo htmlspecialchars($villeAffichee) ?></p>
<p><strong><?= htmlspecialchars($translations['label_code_postal']) ?> :</strong>
<?= htmlspecialchars($praticien['code_postal'] ?? '') ?></p>
<p><strong><?php echo htmlspecialchars($translations['label_code_postal']) ?> :</strong>
<?php echo htmlspecialchars($praticien['code_postal'] ?? '') ?></p>
<p><strong><?= htmlspecialchars($translations['label_adresse']) ?> :</strong><br>
<?= nl2br(htmlspecialchars($praticien['adresse'] ?? '')) ?></p>
<p><strong><?php echo htmlspecialchars($translations['label_adresse']) ?> :</strong><br>
<?php echo nl2br(htmlspecialchars($praticien['adresse'] ?? '')) ?></p>
<p><strong><?= htmlspecialchars($translations['label_presentation']) ?> :</strong><br>
<?= nl2br(htmlspecialchars($praticien['presentation'] ?? '')) ?></p>
<p><strong><?php echo htmlspecialchars($translations['label_presentation']) ?> :</strong><br>
<?php echo nl2br(htmlspecialchars($presentationAffiche)) ?></p>
</div>
</div>
<div class="mt-4 text-center">
<a href="/public/praticien/modifier.php" class="btn btn-primary">
<?= htmlspecialchars($translations['btn_modifier_mes_infos']) ?>
<?php echo htmlspecialchars($translations['btn_modifier_mes_infos']) ?>
</a>
</div>
</div>
@@ -90,11 +117,6 @@ require_once __DIR__ . '/../includes/header.php';
<footer class="site-footer">
<div class="container">
<p><?= $translations['copyr'] ?></p>
<p><?php echo $translations['copyr'] ?></p>
</div>
</footer>
<?php
// Si ton header.php ouvre <body>, pense à fermer proprement dans footer.php.
// Si tu n'as pas de footer commun, laisse comme ça.
?>
+548 -229
View File
@@ -1,289 +1,617 @@
<?php
require_once __DIR__ . '/../includes/init_praticien.php';
require_once __DIR__ . '/../includes/init_praticien.php';
$id = $_SESSION['reference_id'] ?? null;
if (! $id) {
http_response_code(403);
exit('Forbidden');
}
$id = $_SESSION['reference_id'] ?? null;
if (!$id) { http_response_code(403); exit('Forbidden'); }
$currentLang = $lang ?? 'ru';
if (! in_array($currentLang, ['ru', 'fr', 'en'], true)) {
$currentLang = 'ru';
}
$isRu = ($currentLang === 'ru');
// Récupérer les données (pour affichage initial & valeurs par défaut)
$stmt = $pdo->prepare("SELECT * FROM praticien WHERE id = ?");
$stmt->execute([$id]);
$praticien = $stmt->fetch();
if (!$praticien) {
$ro = $isRu ? '' : 'readonly';
$dis = $isRu ? '' : 'disabled';
/**
* Upsert traduction
*/
function upsertPraticienTraduction(PDO $pdo, int $praticienId, string $langue, array $data): void
{
$sql = "
INSERT INTO praticien_traduction (
praticien_id,
langue,
nom_affiche,
prenom_affiche,
ville_affichee,
titre,
specialite,
presentation,
disponibilite,
statut,
created_at,
updated_at
) VALUES (
:praticien_id,
:langue,
:nom_affiche,
:prenom_affiche,
:ville_affichee,
:titre,
:specialite,
:presentation,
:disponibilite,
:statut,
NOW(),
NOW()
)
ON DUPLICATE KEY UPDATE
nom_affiche = VALUES(nom_affiche),
prenom_affiche = VALUES(prenom_affiche),
ville_affichee = VALUES(ville_affichee),
titre = VALUES(titre),
specialite = VALUES(specialite),
presentation = VALUES(presentation),
disponibilite = VALUES(disponibilite),
statut = VALUES(statut),
updated_at = NOW()
";
$stmt = $pdo->prepare($sql);
$stmt->execute([
'praticien_id' => $praticienId,
'langue' => $langue,
'nom_affiche' => $data['nom_affiche'],
'prenom_affiche' => $data['prenom_affiche'],
'ville_affichee' => $data['ville_affichee'],
'titre' => $data['titre'],
'specialite' => $data['specialite'],
'presentation' => $data['presentation'],
'disponibilite' => $data['disponibilite'],
'statut' => $data['statut'],
]);
}
/**
* Lecture traduction par langue
*/
function getPraticienTraduction(PDO $pdo, int $praticienId, string $langue): ?array
{
$stmt = $pdo->prepare("
SELECT *
FROM praticien_traduction
WHERE praticien_id = ? AND langue = ?
LIMIT 1
");
$stmt->execute([$praticienId, $langue]);
$row = $stmt->fetch();
return $row ?: null;
}
/**
* Translittération simple cyrillique -> latin
* Suffisant pour test / affichage.
*/
function transliterateToLatin(string $text): string
{
$map = [
'А' => 'A', 'а' => 'a', 'Б' => 'B', 'б' => 'b', 'В' => 'V', 'в' => 'v', 'Г' => 'G', 'г' => 'g',
'Д' => 'D', 'д' => 'd', 'Е' => 'E', 'е' => 'e', 'Ё' => 'Yo', 'ё' => 'yo', 'Ж' => 'Zh', 'ж' => 'zh',
'З' => 'Z', 'з' => 'z', 'И' => 'I', 'и' => 'i', 'Й' => 'Y', 'й' => 'y', 'К' => 'K', 'к' => 'k',
'Л' => 'L', 'л' => 'l', 'М' => 'M', 'м' => 'm', 'Н' => 'N', 'н' => 'n', 'О' => 'O', 'о' => 'o',
'П' => 'P', 'п' => 'p', 'Р' => 'R', 'р' => 'r', 'С' => 'S', 'с' => 's', 'Т' => 'T', 'т' => 't',
'У' => 'U', 'у' => 'u', 'Ф' => 'F', 'ф' => 'f', 'Х' => 'Kh', 'х' => 'kh', 'Ц' => 'Ts', 'ц' => 'ts',
'Ч' => 'Ch', 'ч' => 'ch', 'Ш' => 'Sh', 'ш' => 'sh', 'Щ' => 'Shch', 'щ' => 'shch',
'Ы' => 'Y', 'ы' => 'y', 'Э' => 'E', 'э' => 'e', 'Ю' => 'Yu', 'ю' => 'yu', 'Я' => 'Ya', 'я' => 'ya',
'Ь' => '', 'ь' => '', 'Ъ' => '', 'ъ' => '',
];
return strtr($text, $map);
}
/**
* Stub traduction.
* Pour l’instant :
* - renvoie le texte tel quel si pas de vrai moteur branché
* - tu pourras remplacer ici par DeepL/API plus tard
*/
function translateText(string $text, string $sourceLang, string $targetLang): string
{
$text = trim($text);
if ($text === '') {
return '';
}
$sourceLang = strtoupper($sourceLang);
$targetLang = strtoupper($targetLang);
if ($sourceLang === $targetLang) {
return $text;
}
if (! defined('DEEPL_API_KEY') || DEEPL_API_KEY === '') {
throw new RuntimeException('Clé API DeepL manquante.');
}
$endpoint = 'https://api-free.deepl.com/v2/translate';
$postFields = http_build_query([
'text' => $text,
'source_lang' => $sourceLang,
'target_lang' => $targetLang,
]);
$ch = curl_init($endpoint);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $postFields,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 20,
CURLOPT_HTTPHEADER => [
'Authorization: DeepL-Auth-Key ' . DEEPL_API_KEY,
'Content-Type: application/x-www-form-urlencoded',
],
]);
$response = curl_exec($ch);
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlErr = curl_error($ch);
curl_close($ch);
if ($response === false || $curlErr) {
throw new RuntimeException('Erreur réseau DeepL : ' . $curlErr);
}
if ($httpCode < 200 || $httpCode >= 300) {
throw new RuntimeException('Erreur DeepL HTTP ' . $httpCode . ' : ' . $response);
}
$data = json_decode($response, true);
if (
! is_array($data)
|| empty($data['translations'])
|| ! isset($data['translations'][0]['text'])
) {
throw new RuntimeException('Réponse DeepL invalide.');
}
return trim($data['translations'][0]['text']);
}
/**
* Détecte si une traduction est manuelle
*/
function isManualTranslation(?array $row): bool
{
return isset($row['statut']) && $row['statut'] === 'manuel';
}
/**
* Synchronise les traductions à partir de la source RU dans praticien
* - ru toujours sync
* - fr/en mis à jour seulement si absents ou non manuels
*/
function syncTranslationsFromSource(PDO $pdo, array $praticien): void
{
$praticienId = (int) $praticien['id'];
$ruData = [
'nom_affiche' => $praticien['nom'] ?? '',
'prenom_affiche' => $praticien['prenom'] ?? '',
'ville_affichee' => $praticien['ville'] ?? '',
'titre' => $praticien['titre'] ?? '',
'specialite' => $praticien['specialite'] ?? '',
'presentation' => $praticien['presentation'] ?? '',
'disponibilite' => $praticien['disponibilite'] ?? '',
'statut' => 'auto',
];
upsertPraticienTraduction($pdo, $praticienId, 'ru', $ruData);
foreach (['fr', 'en'] as $langue) {
$existing = getPraticienTraduction($pdo, $praticienId, $langue);
if ($existing && isManualTranslation($existing)) {
continue;
}
$nomLatin = transliterateToLatin((string) ($praticien['nom'] ?? ''));
$prenomLatin = transliterateToLatin((string) ($praticien['prenom'] ?? ''));
try {
$data = [
'nom_affiche' => $nomLatin,
'prenom_affiche' => $prenomLatin,
'ville_affichee' => translateText((string) ($praticien['ville'] ?? ''), 'RU', $langue),
'titre' => translateText((string) ($praticien['titre'] ?? ''), 'RU', $langue),
'specialite' => translateText((string) ($praticien['specialite'] ?? ''), 'RU', $langue),
'presentation' => translateText((string) ($praticien['presentation'] ?? ''), 'RU', $langue),
'disponibilite' => translateText((string) ($praticien['disponibilite'] ?? ''), 'RU', $langue),
'statut' => 'auto',
];
} catch (Throwable $e) {
error_log('DeepL translation error [' . $langue . '] praticien_id=' . $praticienId . ' : ' . $e->getMessage());
//die($e->getMessage());
$data = [
'nom_affiche' => $nomLatin,
'prenom_affiche' => $prenomLatin,
'ville_affichee' => (string) ($praticien['ville'] ?? ''),
'titre' => (string) ($praticien['titre'] ?? ''),
'specialite' => (string) ($praticien['specialite'] ?? ''),
'presentation' => (string) ($praticien['presentation'] ?? ''),
'disponibilite' => (string) ($praticien['disponibilite'] ?? ''),
'statut' => 'erreur',
];
}
upsertPraticienTraduction($pdo, $praticienId, $langue, $data);
}
}
// Base praticien
$stmt = $pdo->prepare("SELECT * FROM praticien WHERE id = ?");
$stmt->execute([$id]);
$praticien = $stmt->fetch();
if (! $praticien) {
die(htmlspecialchars($translations['praticien_introuvable'] ?? 'Introuvable.'));
}
}
// ==== Traitement POST avant tout output ====
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Traduction langue courante
$traduction = getPraticienTraduction($pdo, (int) $id, $currentLang);
// Valeurs formulaire avec fallback
$formNom = $traduction['nom_affiche'] ?? $praticien['nom'] ?? '';
$formPrenom = $traduction['prenom_affiche'] ?? $praticien['prenom'] ?? '';
$formVilleAffichee = $traduction['ville_affichee'] ?? $praticien['ville'] ?? '';
$formTitre = $traduction['titre'] ?? $praticien['titre'] ?? '';
$formSpecialite = $traduction['specialite'] ?? $praticien['specialite'] ?? '';
$formPresentation = $traduction['presentation'] ?? $praticien['presentation'] ?? '';
$formDispo = $traduction['disponibilite'] ?? $praticien['disponibilite'] ?? '';
// ==== POST ====
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
require_post_csrf();
$titre = trim($_POST['titre'] ?? '');
$nom = trim($_POST['nom'] ?? '');
$prenom = trim($_POST['prenom'] ?? '');
$nomAffiche = trim($_POST['nom'] ?? '');
$prenomAffiche = trim($_POST['prenom'] ?? '');
$telephone = trim($_POST['telephone'] ?? '');
$specialite = trim($_POST['specialite'] ?? '');
$adresse = trim($_POST['adresse'] ?? '');
$ville = trim($_POST['ville'] ?? '');
$code_postal = trim($_POST['code_postal'] ?? '');
$presentation = trim($_POST['presentation'] ?? '');
$duree_rdv = (int)($_POST['duree_rdv'] ?? 60);
$duree_rdv = (int) ($_POST['duree_rdv'] ?? 60);
$disponibilite = trim($_POST['disponibilite'] ?? '');
$photo = $praticien['photo'] ?? null;
if ($specialite === '' || $presentation === '') {
die(htmlspecialchars($translations['err_champs_obligatoires'] ?? 'Merci de remplir les champs obligatoires.'));
}
// En FR/EN on ignore tout update des champs globaux même si quelqu’un bidouille le HTML
if (! $isRu) {
$telephone = $praticien['telephone'] ?? '';
$adresse = $praticien['adresse'] ?? '';
$ville = $praticien['ville'] ?? '';
$code_postal = $praticien['code_postal'] ?? '';
$duree_rdv = (int) ($praticien['duree_rdv'] ?? 60);
}
// Upload photo : uniquement RU
if ($isRu && ! empty($_FILES['photo']['tmp_name'])) {
$file = $_FILES['photo'];
$maxSize = 5 * 1024 * 1024;
// --- BLOC UPLOAD SÉCURISÉ ---
if (!empty($_FILES['photo']['tmp_name'])) {
$file = $_FILES['photo'];
$maxSize = 5 * 1024 * 1024; // 5 Mo
// 1. Vérification de la taille
if ($file['size'] > $maxSize) {
die("Erreur : Le fichier est trop volumineux (max 5 Mo).");
}
// 2. Vérification du type réel (MIME) pour bloquer les faux fichiers
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($file['tmp_name']);
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($file['tmp_name']);
$allowedMimeTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!in_array($mimeType, $allowedMimeTypes)) {
if (! in_array($mimeType, $allowedMimeTypes, true)) {
die("Erreur : Type de fichier non autorisé (Seuls JPG, PNG, GIF et WEBP sont acceptés).");
}
// 3. Préparation du nom et du chemin
$nom_nettoye = strtolower(preg_replace('/[^A-Za-z0-9]/', '', $nom));
$prenom_nettoye = strtolower(preg_replace('/[^A-Za-z0-9]/', '', $prenom));
$nom_nettoye = strtolower(preg_replace('/[^A-Za-z0-9]/', '', transliterateToLatin($nomAffiche)));
$prenom_nettoye = strtolower(preg_replace('/[^A-Za-z0-9]/', '', transliterateToLatin($prenomAffiche)));
$nouveau_nom_fichier = $nom_nettoye . '_' . $prenom_nettoye . '_' . time() . '.png';
$chemin_destination = __DIR__ . '/../uploads/' . $nouveau_nom_fichier;
$chemin_destination = __DIR__ . '/../uploads/' . $nouveau_nom_fichier;
// 4. Traitement de l'image (Conversion & Redimensionnement)
$image_source = @imagecreatefromstring(file_get_contents($file['tmp_name']));
if ($image_source) {
list($largeur_orig, $hauteur_orig) = getimagesize($file['tmp_name']);
$largeur_max = 800;
if ($largeur_orig > $largeur_max) {
$ratio = $largeur_max / $largeur_orig;
$nouvelle_largeur = $largeur_max;
$nouvelle_hauteur = (int)($hauteur_orig * $ratio);
} else {
$nouvelle_largeur = $largeur_orig;
$nouvelle_hauteur = $hauteur_orig;
}
$image_dest = imagecreatetruecolor($nouvelle_largeur, $nouvelle_hauteur);
imagealphablending($image_dest, false);
imagesavealpha($image_dest, true);
imagecopyresampled($image_dest, $image_source, 0, 0, 0, 0, $nouvelle_largeur, $nouvelle_hauteur, $largeur_orig, $hauteur_orig);
// Sauvegarde effective
if (imagepng($image_dest, $chemin_destination, 7)) {
// Suppression de l'ancienne photo si elle existe
if (!empty($praticien['photo'])) {
$ancien_chemin = __DIR__ . '/../uploads/' . $praticien['photo'];
if (file_exists($ancien_chemin)) {
unlink($ancien_chemin);
}
}
$photo = $nouveau_nom_fichier;
}
imagedestroy($image_source);
imagedestroy($image_dest);
} else {
if (! $image_source) {
die("Erreur : Le fichier image semble corrompu.");
}
}
// SIRET - normalisation + vérifs
$siret = preg_replace('/\D/', '', $_POST['siret'] ?? '');
if (strlen($siret) !== 14) {
die(htmlspecialchars($translations['siret_format_invalide'] ?? 'SIRET invalide (14 chiffres).'));
}
// Luhn
$luhnOk = (function($num){
$sum=0; $alt=false;
for ($i=strlen($num)-1; $i>=0; $i--){
$n=(int)$num[$i];
if($alt){ $n*=2; if($n>9)$n-=9; }
$sum+=$n; $alt=!$alt;
[$largeur_orig, $hauteur_orig] = getimagesize($file['tmp_name']);
$largeur_max = 800;
if ($largeur_orig > $largeur_max) {
$ratio = $largeur_max / $largeur_orig;
$nouvelle_largeur = $largeur_max;
$nouvelle_hauteur = (int) ($hauteur_orig * $ratio);
} else {
$nouvelle_largeur = $largeur_orig;
$nouvelle_hauteur = $hauteur_orig;
}
return $sum%10===0;
})($siret);
if (!$luhnOk) {
die(htmlspecialchars($translations['siret_format_invalide'] ?? 'SIRET invalide (Luhn).'));
$image_dest = imagecreatetruecolor($nouvelle_largeur, $nouvelle_hauteur);
imagealphablending($image_dest, false);
imagesavealpha($image_dest, true);
imagecopyresampled(
$image_dest,
$image_source,
0, 0, 0, 0,
$nouvelle_largeur,
$nouvelle_hauteur,
$largeur_orig,
$hauteur_orig
);
if (imagepng($image_dest, $chemin_destination, 7)) {
if (! empty($praticien['photo'])) {
$ancien_chemin = __DIR__ . '/../uploads/' . $praticien['photo'];
if (file_exists($ancien_chemin)) {
unlink($ancien_chemin);
}
}
$photo = $nouveau_nom_fichier;
}
imagedestroy($image_source);
imagedestroy($image_dest);
}
// Unicité SIRET (exclure l'utilisateur courant)
$chk = $pdo->prepare("SELECT id FROM praticien WHERE siret = :s AND id <> :id LIMIT 1");
$chk->execute(['s' => $siret, 'id' => $id]);
if ($chk->fetch()) {
die(htmlspecialchars($translations['err_siret_deja_utilise'] ?? 'Ce SIRET est déjà utilisé.'));
$pdo->beginTransaction();
try {
if ($isRu) {
$siret = preg_replace('/\D/', '', $_POST['siret'] ?? '');
if (strlen($siret) !== 14) {
throw new RuntimeException($translations['siret_format_invalide'] ?? 'SIRET invalide (14 chiffres).');
}
$luhnOk = (function ($num) {
$sum = 0;
$alt = false;
for ($i = strlen($num) - 1; $i >= 0; $i--) {
$n = (int) $num[$i];
if ($alt) {
$n *= 2;
if ($n > 9) {
$n -= 9;
}
}
$sum += $n;
$alt = ! $alt;
}
return $sum % 10 === 0;
})($siret);
if (! $luhnOk) {
throw new RuntimeException($translations['siret_format_invalide'] ?? 'SIRET invalide (Luhn).');
}
$chk = $pdo->prepare("SELECT id FROM praticien WHERE siret = :s AND id <> :id LIMIT 1");
$chk->execute(['s' => $siret, 'id' => $id]);
if ($chk->fetch()) {
throw new RuntimeException($translations['err_siret_deja_utilise'] ?? 'Ce SIRET est déjà utilisé.');
}
$stmt = $pdo->prepare("
UPDATE praticien SET
titre = :titre,
nom = :nom,
prenom = :prenom,
telephone = :telephone,
specialite = :specialite,
adresse = :adresse,
ville = :ville,
code_postal = :code_postal,
presentation = :presentation,
photo = :photo,
disponibilite = :disponibilite,
duree_rdv = :duree_rdv,
siret = :siret,
langue_source = 'ru'
WHERE id = :id
");
$stmt->execute([
'titre' => $titre,
'nom' => $nomAffiche,
'prenom' => $prenomAffiche,
'telephone' => $telephone,
'specialite' => $specialite,
'adresse' => $adresse,
'ville' => $ville,
'code_postal' => $code_postal,
'presentation' => $presentation,
'photo' => $photo,
'disponibilite' => $disponibilite,
'duree_rdv' => $duree_rdv,
'siret' => $siret,
'id' => $id,
]);
// Recharge la source après update
$stmt = $pdo->prepare("SELECT * FROM praticien WHERE id = ?");
$stmt->execute([$id]);
$praticien = $stmt->fetch();
if (! $praticien) {
throw new RuntimeException('Praticien introuvable après mise à jour.');
}
syncTranslationsFromSource($pdo, $praticien);
} else {
upsertPraticienTraduction($pdo, (int) $id, $currentLang, [
'nom_affiche' => $nomAffiche,
'prenom_affiche' => $prenomAffiche,
'ville_affichee' => $traduction['ville_affichee'] ?? $praticien['ville'] ?? '',
'titre' => $titre,
'specialite' => $specialite,
'presentation' => $presentation,
'disponibilite' => $disponibilite,
'statut' => 'manuel',
]);
}
$pdo->commit();
} catch (Throwable $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
die(htmlspecialchars($e->getMessage()));
}
// Mise à jour
$stmt = $pdo->prepare("
UPDATE praticien SET
titre = :titre,
nom = :nom,
prenom = :prenom,
telephone = :telephone,
specialite = :specialite,
adresse = :adresse,
ville = :ville,
code_postal = :code_postal,
presentation = :presentation,
photo = :photo,
disponibilite = :disponibilite,
duree_rdv = :duree_rdv,
siret = :siret
WHERE id = :id
");
$stmt->execute([
'titre' => $titre,
'nom' => $nom,
'prenom' => $prenom,
'telephone' => $telephone,
'specialite' => $specialite,
'adresse' => $adresse,
'ville' => $ville,
'code_postal' => $code_postal,
'presentation' => $presentation,
'photo' => $photo,
'disponibilite' => $disponibilite,
'duree_rdv' => $duree_rdv,
'siret' => $siret,
'id' => $id,
]);
// Redirection ABSOLUE (ou "informations.php" si même dossier)
header('Location: /public/praticien/informations.php');
exit;
}
$PAGE_TITLE = $translations['modifier_infos_title'];
$HEAD_EXTRA = <<<HTML
<style>
.message-erreur {
color: red;
font-size: 0.9em;
display: none;
margin-bottom: 10px;
}
</style>
$PAGE_TITLE = $translations['modifier_infos_title'];
$HEAD_EXTRA = <<<HTML
<style>
.message-erreur {
color: red;
font-size: 0.9em;
display: none;
margin-bottom: 10px;
}
</style>
HTML;
// ==== À partir d’ici, on peut afficher ====
require_once __DIR__ . '/../includes/header.php';
require_once __DIR__ . '/../includes/header.php';
?>
<div class="main1">
<div class="container py-5">
<form id="form-praticien-edit" method="POST" enctype="multipart/form-data" class="shadow p-4 rounded bg-light">
<h2 class="text-center mb-4"><?= htmlspecialchars($translations['modifier_infos_title']) ?></h2>
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(ensure_csrf_token(), ENT_QUOTES, 'UTF-8'); ?>">
<h2 class="text-center mb-4"><?php echo htmlspecialchars($translations['modifier_infos_title']) ?></h2>
<div class="mb-3">
<label class="form-label"><?= htmlspecialchars($translations['label_nom']) ?> *</label>
<input type="text" name="nom" class="form-control" value="<?= htmlspecialchars($praticien['nom']?? '') ?>" required>
<label class="form-label"><?php echo htmlspecialchars($translations['label_nom']) ?> *</label>
<input type="text" name="nom" class="form-control" value="<?php echo htmlspecialchars($formNom) ?>" required>
</div>
<div class="mb-3">
<label class="form-label"><?= htmlspecialchars($translations['label_prenom']) ?> *</label>
<input type="text" name="prenom" class="form-control" value="<?= htmlspecialchars($praticien['prenom']?? '') ?>" required>
<label class="form-label"><?php echo htmlspecialchars($translations['label_prenom']) ?> *</label>
<input type="text" name="prenom" class="form-control" value="<?php echo htmlspecialchars($formPrenom) ?>" required>
</div>
<div class="mb-3">
<label class="form-label"><?= htmlspecialchars($translations['label_telephone']) ?> *</label>
<input type="tel" name="telephone" id="telephone" class="form-control" value="<?= htmlspecialchars($praticien['telephone']?? '') ?>" required placeholder="+33600000000"
pattern="^\+?[1-9]\d{6,14}$"
title="<?=htmlspecialchars($translations['telephone_format_message']?? '') ?>">
<label class="form-label"><?php echo htmlspecialchars($translations['label_telephone']) ?> *</label>
<input type="tel" name="telephone" id="telephone" class="form-control"
value="<?php echo htmlspecialchars($praticien['telephone'] ?? '') ?>"
required
placeholder="+33600000000"
pattern="^\+?[1-9]\d{6,14}$"
title="<?php echo htmlspecialchars($translations['telephone_format_message'] ?? '') ?>"
<?php echo $ro; ?>>
</div>
<!-- Champ SIREN/SIRET -->
<div class="mb-3">
<label class="form-label"><?= htmlspecialchars($translations['placeholder_siret']) ?> *</label>
<input type="text"
name="siret"
id="siret"
class="form-control"
value="<?= htmlspecialchars($praticien['siret']?? '') ?>"
required
maxlength="17"
inputmode="numeric"
pattern="[0-9 ]+">
<div id="siret-feedback" class="message-erreur" style="display:none;"></div>
</div>
<div class="mb-3">
<label class="form-label"><?= htmlspecialchars($translations['label_specialite']) ?> *</label>
<input type="text" name="specialite" class="form-control" value="<?= htmlspecialchars($praticien['specialite']?? '') ?>" required>
<label class="form-label"><?php echo htmlspecialchars($translations['placeholder_siret']) ?> *</label>
<input type="text"
name="siret"
id="siret"
class="form-control"
value="<?php echo htmlspecialchars($praticien['siret'] ?? '') ?>"
required
maxlength="17"
inputmode="numeric"
pattern="[0-9 ]+"
>
<div id="siret-feedback" class="message-erreur" style="display:none;"></div>
</div>
<div class="mb-3">
<label class="form-label"><?= htmlspecialchars($translations['tarif_consultation']) ?></label>
<input type="text" name="titre" class="form-control" value="<?= htmlspecialchars($praticien['titre']?? '') ?>">
<div class="mb-3">
<label class="form-label"><?php echo htmlspecialchars($translations['label_specialite']) ?> *</label>
<input type="text" name="specialite" class="form-control" value="<?php echo htmlspecialchars($formSpecialite) ?>" required>
</div>
<div class="mb-3">
<label class="form-label"><?= htmlspecialchars($translations['moyens_payement']) ?></label>
<input type="text" name="disponibilite" class="form-control" value="<?= htmlspecialchars($praticien['disponibilite']?? '') ?>">
<div class="mb-3">
<label class="form-label"><?php echo htmlspecialchars($translations['tarif_consultation']) ?></label>
<input type="text" name="titre" class="form-control" value="<?php echo htmlspecialchars($formTitre) ?>">
</div>
<div class="mb-3">
<label class="form-label"><?php echo htmlspecialchars($translations['moyens_payement']) ?></label>
<input type="text" name="disponibilite" class="form-control" value="<?php echo htmlspecialchars($formDispo) ?>">
</div>
<div class="mb-3 position-relative">
<label class="form-label"><?= htmlspecialchars($translations['label_adresse']) ?></label>
<textarea name="adresse" id="adresse" class="form-control"><?= htmlspecialchars($praticien['adresse']?? '') ?></textarea>
<label class="form-label"><?php echo htmlspecialchars($translations['label_adresse']) ?></label>
<textarea name="adresse" id="adresse" class="form-control" <?php echo $ro; ?>><?php echo htmlspecialchars($praticien['adresse'] ?? '') ?></textarea>
<ul id="suggestions"></ul>
</div>
<div class="mb-3">
<label class="form-label"><?= htmlspecialchars($translations['label_ville']) ?></label>
<input type="text" name="ville" id="ville" class="form-control" value="<?= htmlspecialchars($praticien['ville']?? '') ?>">
</div>
<div class="mb-3">
<label class="form-label"><?= htmlspecialchars($translations['label_code_postal']) ?></label>
<input type="text" name="code_postal" id="code_postal" class="form-control" value="<?= htmlspecialchars($praticien['code_postal']?? '') ?>">
</div>
<div class="mb-3">
<label class="form-label"><?= htmlspecialchars($translations['label_presentation']) ?></label>
<textarea name="presentation" class="form-control"><?= htmlspecialchars($praticien['presentation']?? '') ?></textarea>
<label class="form-label"><?php echo htmlspecialchars($translations['label_ville']) ?></label>
<input type="text" name="ville" id="ville" class="form-control"
value="<?php echo htmlspecialchars($praticien['ville'] ?? '') ?>" readonly>
</div>
<div class="mb-3">
<label class="form-label"><?= htmlspecialchars($translations['label_duree_rdv']) ?></label>
<input type="number" name="duree_rdv" class="form-control" value="<?= $praticien['duree_rdv'] ?>" min="15" max="180" required>
<label class="form-label"><?php echo htmlspecialchars($translations['label_code_postal']) ?></label>
<input type="text" name="code_postal" id="code_postal" class="form-control"
value="<?php echo htmlspecialchars($praticien['code_postal'] ?? '') ?>"
<?php echo $ro; ?>>
</div>
<div class="mb-3">
<label class="form-label"><?= htmlspecialchars($translations['label_photo']) ?></label>
<input type="file" name="photo" class="form-control" accept="image/*">
<label class="form-label"><?php echo htmlspecialchars($translations['label_presentation']) ?> *</label>
<textarea name="presentation" class="form-control" required><?php echo htmlspecialchars($formPresentation) ?></textarea>
</div>
<?php if ($praticien['photo']): ?>
<div class="mb-3">
<label class="form-label"><?php echo htmlspecialchars($translations['label_duree_rdv']) ?></label>
<input type="number" name="duree_rdv" class="form-control"
value="<?php echo (int) ($praticien['duree_rdv'] ?? 60); ?>"
min="15" max="180" required
<?php echo $ro; ?>>
</div>
<div class="mb-3">
<label class="form-label"><?php echo htmlspecialchars($translations['label_photo']) ?></label>
<input type="file" name="photo" class="form-control" accept="image/*" <?php echo $dis; ?>>
</div>
<?php if (! empty($praticien['photo'])): ?>
<div class="mb-3">
<p><?= htmlspecialchars($translations['photo_actuelle']) ?></p>
<img src="/public/uploads/<?= htmlspecialchars($praticien['photo']) ?>" class="img-fluid rounded-4 shadow" style="width:230px;height:auto;">
<p><?php echo htmlspecialchars($translations['photo_actuelle']) ?></p>
<img src="/public/uploads/<?php echo htmlspecialchars($praticien['photo']) ?>"
class="img-fluid rounded-4 shadow"
style="width:230px;height:auto;">
</div>
<?php endif; ?>
<div class="mt-4 text-center">
<button type="submit" class="btn btn-primary"><?= htmlspecialchars($translations['btn_enregistrer']) ?></button>
<div class="mt-4 text-center">
<button type="submit" class="btn btn-primary"><?php echo htmlspecialchars($translations['btn_enregistrer']) ?></button>
</div>
<?= $translations['champs_obligatoire'] ?>
<?php echo $translations['champs_obligatoire'] ?>
</form>
</div>
</form>
<script>
// === Téléphone: simple & efficace ===
const tel = document.getElementById('telephone');
// 1) Nettoyage en direct (espaces, tirets, points, parenthèses)
tel?.addEventListener('input', function () {
const pos = this.selectionStart;
this.value = this.value.replace(/[\s\-._()/]/g, '');
try { this.setSelectionRange(pos, pos); } catch(_) {}
});
// 2) Normalisation au blur
tel?.addEventListener('blur', function() {
let val = this.value.trim().replace(/[\s\-._()/]/g, '');
if (val.startsWith('00')) val = '+' + val.slice(2);
@@ -292,7 +620,6 @@ tel?.addEventListener('blur', function() {
this.value = val;
});
// 3) Au submit : applique la normalisation même si l’utilisateur n’a pas quitté le champ
tel?.form?.addEventListener('submit', function (e) {
tel.dispatchEvent(new Event('blur'));
if (!tel.checkValidity()) {
@@ -304,23 +631,24 @@ tel?.form?.addEventListener('submit', function (e) {
document.addEventListener("DOMContentLoaded", function () {
const adresseInput = document.getElementById("adresse");
const villeInput = document.getElementById("ville");
const cpInput = document.getElementById("code_postal");
const suggestions = document.getElementById("suggestions");
const villeInput = document.getElementById("ville");
const cpInput = document.getElementById("code_postal");
const suggestions = document.getElementById("suggestions");
if (adresseInput.hasAttribute('readonly')) {
return;
}
// petite fonction utilitaire : construit "numero + rue"
function buildStreet(props) {
const num = (props.housenumber || '').trim();
const street = (props.street || props.name || '').trim();
return [num, street].filter(Boolean).join(' ');
}
// fallback ville si city manquant (rare)
function getCity(props) {
if (props.city) return props.city;
// ex: "84160, Cucuron, Vaucluse, Provence-Alpes-Côte d'Azur"
if (props.context) {
const parts = props.context.split(',').map(s => s.trim());
// généralement la 2e valeur correspond à la commune
return parts[1] || parts[0] || '';
}
return '';
@@ -335,7 +663,6 @@ document.addEventListener("DOMContentLoaded", function () {
return;
}
// annule la requête précédente si on retape vite
try { abortCtrl?.abort(); } catch(_) {}
abortCtrl = new AbortController();
@@ -352,9 +679,7 @@ document.addEventListener("DOMContentLoaded", function () {
li.textContent = props.label || buildStreet(props);
li.addEventListener("click", function () {
// ✅ Adresse = seulement numéro + rue
adresseInput.value = buildStreet(props);
// ✅ Ville + CP séparés
villeInput.value = getCity(props);
cpInput.value = props.postcode || '';
suggestions.innerHTML = '';
@@ -366,7 +691,6 @@ document.addEventListener("DOMContentLoaded", function () {
.catch(() => {});
});
// clic hors zone -> ferme la liste
document.addEventListener("click", function (e) {
if (!suggestions.contains(e.target) && e.target !== adresseInput) {
suggestions.innerHTML = '';
@@ -375,11 +699,7 @@ document.addEventListener("DOMContentLoaded", function () {
});
</script>
<!--***********************************************************
Verification ajax pour SIRET
**************************************************************-->
<script>
// Luhn SIRET
function luhnCheck(num) {
const arr = (num + '').split('').reverse().map(x => parseInt(x, 10));
const sum = arr.reduce((acc, val, i) => {
@@ -390,21 +710,24 @@ function luhnCheck(num) {
}
(function(){
const form = document.getElementById('form-praticien-edit'); // <-- bon id
const form = document.getElementById('form-praticien-edit');
const siretInput = document.getElementById('siret');
const feedback = document.getElementById('siret-feedback');
// init
if (!siretInput || siretInput.hasAttribute('readonly')) {
return;
}
form.dataset.siretExists = '0';
let debounceId = null;
let lastQueried = '';
const MSG = {
invalide: '<?= addslashes($translations["siret_format_invalide"] ?? "Numéro SIRET invalide.") ?>',
valide: '<?= addslashes($translations["siret_valide_format"] ?? "Format SIRET valide.") ?>',
existe: '<?= addslashes($translations["siret_trouve"] ?? "Numéro reconnu dans la base INSEE.") ?>',
nexiste: '<?= addslashes($translations["siret_non_trouve"] ?? "Numéro introuvable dans la base INSEE.") ?>',
attente: '<?= addslashes($translations["siret_verification_en_cours"] ?? "Vérification en cours…") ?>'
invalide: '<?php echo addslashes($translations["siret_format_invalide"] ?? "Numéro SIRET invalide.") ?>',
valide: '<?php echo addslashes($translations["siret_valide_format"] ?? "Format SIRET valide.") ?>',
existe: '<?php echo addslashes($translations["siret_trouve"] ?? "Numéro reconnu dans la base INSEE.") ?>',
nexiste: '<?php echo addslashes($translations["siret_non_trouve"] ?? "Numéro introuvable dans la base INSEE.") ?>',
attente: '<?php echo addslashes($translations["siret_verification_en_cours"] ?? "Vérification en cours…") ?>'
};
function formatSiret(value) {
@@ -414,13 +737,13 @@ function luhnCheck(num) {
if (value.length > 11) value = value.slice(0,11) + ' ' + value.slice(11);
return value;
}
function setFeedback(text, color) {
feedback.textContent = text;
feedback.style.color = color;
feedback.style.display = 'block';
}
// Pré-formate la valeur initiale si elle est brute (14 chiffres)
document.addEventListener('DOMContentLoaded', () => {
if (/^\d{14}$/.test(siretInput.value)) {
siretInput.value = formatSiret(siretInput.value);
@@ -428,7 +751,6 @@ function luhnCheck(num) {
});
function checkSiretImmediate() {
// formatage visuel
const formatted = formatSiret(siretInput.value);
if (siretInput.value !== formatted) siretInput.value = formatted;
@@ -446,7 +768,7 @@ function luhnCheck(num) {
clearTimeout(debounceId);
debounceId = setTimeout(() => {
fetch('/../../js/ajax_verif_siret.php?siret=' + encodeURIComponent(raw)) // <-- chemin ABSOLU
fetch('/../../js/ajax_verif_siret.php?siret=' + encodeURIComponent(raw))
.then(r => r.json())
.then(data => {
if (data && data.exists) {
@@ -466,7 +788,6 @@ function luhnCheck(num) {
siretInput.addEventListener('input', checkSiretImmediate);
// Normalise AVANT envoi (supprime les espaces)
form.addEventListener('submit', function(e){
const raw = siretInput.value.replace(/\s+/g, '');
if (raw.length !== 14 || !luhnCheck(raw)) {
@@ -475,9 +796,7 @@ function luhnCheck(num) {
siretInput.focus();
return;
}
siretInput.value = raw; // <- on poste 14 chiffres à PHP
// Si tu veux forcer l’existence INSEE côté front, décommente :
// if (form.dataset.siretExists !== '1') { e.preventDefault(); setFeedback(MSG.nexiste, 'red'); siretInput.focus(); }
siretInput.value = raw;
});
})();
</script>
@@ -485,7 +804,7 @@ function luhnCheck(num) {
</div>
<footer class="site-footer">
<div class="container">
<p><?= $translations['copyr'] ?></p>
<p><?php echo $translations['copyr'] ?></p>
</div>
</footer>
</body>
@@ -0,0 +1,80 @@
<?php
declare (strict_types = 1);
require_once __DIR__ . '/../includes/init_praticien.php';
header('Content-Type: application/json; charset=utf-8');
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
echo json_encode([
'ok' => false,
'message' => 'Méthode non autorisée.',
], JSON_UNESCAPED_UNICODE);
exit;
}
require_post_csrf();
$praticien_id = (int) ($_SESSION['reference_id'] ?? 0);
if ($praticien_id <= 0) {
echo json_encode([
'ok' => false,
'message' => 'Praticien non autorisé.',
], JSON_UNESCAPED_UNICODE);
exit;
}
$id = (int) ($_POST['id'] ?? 0);
if ($id <= 0) {
echo json_encode([
'ok' => false,
'message' => 'ID invalide.',
], JSON_UNESCAPED_UNICODE);
exit;
}
try {
$check = $pdo->prepare("
SELECT id, praticien_id
FROM disponibilite
WHERE id = ?
LIMIT 1
");
$check->execute([$id]);
$slot = $check->fetch(PDO::FETCH_ASSOC);
if (! $slot) {
echo json_encode([
'ok' => false,
'message' => 'Créneau introuvable.',
], JSON_UNESCAPED_UNICODE);
exit;
}
if ((int) $slot['praticien_id'] !== $praticien_id) {
echo json_encode([
'ok' => false,
'message' => 'Ce créneau ne vous appartient pas.',
], JSON_UNESCAPED_UNICODE);
exit;
}
$stmt = $pdo->prepare("DELETE FROM disponibilite WHERE id = ? AND praticien_id = ?");
$stmt->execute([$id, $praticien_id]);
echo json_encode([
'ok' => ($stmt->rowCount() > 0),
'message' => $stmt->rowCount() > 0
? 'Créneau supprimé.'
: 'Aucune ligne supprimée.',
], JSON_UNESCAPED_UNICODE);
exit;
} catch (Throwable $e) {
error_log('supprimer_disponibilite.php: ' . $e->getMessage());
echo json_encode([
'ok' => false,
'message' => 'Erreur serveur.',
], JSON_UNESCAPED_UNICODE);
exit;
}
@@ -0,0 +1,80 @@
<?php
declare (strict_types = 1);
require_once __DIR__ . '/../includes/init_praticien.php';
header('Content-Type: application/json; charset=utf-8');
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
echo json_encode([
'ok' => false,
'message' => 'Méthode non autorisée.',
], JSON_UNESCAPED_UNICODE);
exit;
}
require_post_csrf();
$praticien_id = (int) ($_SESSION['reference_id'] ?? 0);
if ($praticien_id <= 0) {
echo json_encode([
'ok' => false,
'message' => 'Accès refusé.',
], JSON_UNESCAPED_UNICODE);
exit;
}
$id = (int) ($_POST['id'] ?? 0);
if ($id <= 0) {
echo json_encode([
'ok' => false,
'message' => 'ID invalide.',
], JSON_UNESCAPED_UNICODE);
exit;
}
try {
$check = $pdo->prepare("
SELECT id, praticien_id
FROM disponibilite_date
WHERE id = ?
LIMIT 1
");
$check->execute([$id]);
$slot = $check->fetch(PDO::FETCH_ASSOC);
if (! $slot) {
echo json_encode([
'ok' => false,
'message' => 'Créneau introuvable.',
], JSON_UNESCAPED_UNICODE);
exit;
}
if ((int) $slot['praticien_id'] !== $praticien_id) {
echo json_encode([
'ok' => false,
'message' => 'Ce créneau ne vous appartient pas.',
], JSON_UNESCAPED_UNICODE);
exit;
}
$stmt = $pdo->prepare("DELETE FROM disponibilite_date WHERE id = ? AND praticien_id = ?");
$stmt->execute([$id, $praticien_id]);
echo json_encode([
'ok' => ($stmt->rowCount() > 0),
'message' => $stmt->rowCount() > 0
? 'Créneau supprimé.'
: 'Aucune ligne supprimée.',
], JSON_UNESCAPED_UNICODE);
exit;
} catch (Throwable $e) {
error_log('praticien/supprimer_disponibilite_date.php: ' . $e->getMessage());
echo json_encode([
'ok' => false,
'message' => 'Erreur serveur.',
], JSON_UNESCAPED_UNICODE);
exit;
}