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;
}