prepare("SELECT nom, prenom, duree_rdv, adresse, ville, code_postal FROM praticien WHERE id = ?");
$stmt->execute([$praticien_id]);
$praticien = $stmt->fetch(PDO::FETCH_ASSOC);
$duree_minutes = (int) ($praticien['duree_rdv'] ?? 0);
if (! $duree_minutes) {
die($translations['duree_rdv_non_definie']);
}
// Adresse complète ?
$has_address =
! empty(trim((string) ($praticien['adresse'] ?? ''))) &&
! empty(trim((string) ($praticien['ville'] ?? ''))) &&
! empty(trim((string) ($praticien['code_postal'] ?? '')));
// TZ + fenêtre 21 jours
$tz = new DateTimeZone('Europe/Paris');
$now = new DateTimeImmutable('now', $tz);
$limit = $now->modify('+60 days'); // exclusif dans DatePeriod
// Récupération des disponibilités HEBDO
$stmt = $pdo->prepare("SELECT jour_semaine, heure_debut, heure_fin FROM disponibilite WHERE praticien_id = ?");
$stmt->execute([$praticien_id]);
$dispos = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Récupération des disponibilités PONCTUELLES (dates)
$stmt = $pdo->prepare(
"SELECT date_dispo, heure_debut, heure_fin
FROM disponibilite_date
WHERE praticien_id = ?
AND date_dispo BETWEEN ? AND ?
ORDER BY date_dispo ASC, heure_debut ASC"
);
$stmt->execute([
$praticien_id,
$now->format('Y-m-d'),
$limit->format('Y-m-d'),
]);
$dispos_dates_raw = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Index par date (Y-m-d) pour accès rapide dans la boucle
$dispos_dates_by_day = [];
foreach ($dispos_dates_raw as $row) {
$k = (string) $row['date_dispo']; // Y-m-d
$dispos_dates_by_day[$k][] = $row;
}
// Récupération des rendez-vous réservés (prochains 21 jours) — on ignore les ANNULÉS
$stmt = $pdo->prepare(
"SELECT date_rdv
FROM rendez_vous
WHERE praticien_id = ?
AND statut <> 'annulé'
AND date_rdv BETWEEN ? AND ?"
);
$stmt->execute([$praticien_id, $now->format('Y-m-d H:i:s'), $limit->format('Y-m-d H:i:s')]);
$reservations = array_map(
fn($r) => (new DateTimeImmutable($r['date_rdv'], $tz))->format('Y-m-d H:i'),
$stmt->fetchAll()
);
// Map canon EN -> clé i18n existante
$DAY_I18N = [
'monday' => 'jour_lundi',
'tuesday' => 'jour_mardi',
'wednesday' => 'jour_mercredi',
'thursday' => 'jour_jeudi',
'friday' => 'jour_vendredi',
'saturday' => 'jour_samedi',
'sunday' => 'jour_dimanche',
];
// Si ta DB ne stocke pas en EN (lundi/Понедельник), normalise vers EN :
function normalize_day_key(string $label): ?string
{
$l = mb_strtolower(trim($label));
static $map = [
// EN
'monday' => 'monday', 'tuesday' => 'tuesday', 'wednesday' => 'wednesday',
'thursday' => 'thursday', 'friday' => 'friday', 'saturday' => 'saturday', 'sunday' => 'sunday',
// FR
'lundi' => 'monday', 'mardi' => 'tuesday', 'mercredi' => 'wednesday',
'jeudi' => 'thursday', 'vendredi' => 'friday', 'samedi' => 'saturday', 'dimanche' => 'sunday',
// RU
'понедельник' => 'monday', 'вторник' => 'tuesday', 'среда' => 'wednesday',
'четверг' => 'thursday', 'пятница' => 'friday', 'суббота' => 'saturday', 'воскресенье' => 'sunday',
];
return $map[$l] ?? null;
}
// Ici on stocke en SET pour fusionner sans doublons : [jourAffichage][slotKey] = DateTimeImmutable
$creneaux_par_jour = [];
$periode = new DatePeriod($now, new DateInterval('P1D'), $limit); // $limit exclusif
$step = new DateInterval("PT{$duree_minutes}M");
foreach ($periode as $jour) {
$dayKey = strtolower($jour->format('l')); // monday..sunday
$dateKey = $jour->format('Y-m-d');
// Libellé traduit pour l'affichage
$i18nKey = $DAY_I18N[$dayKey] ?? null;
$jour_nom = $i18nKey ? ($translations[$i18nKey] ?? ucfirst($dayKey)) : ucfirst($dayKey);
// clé colonne (inchangée)
$colKey = $jour_nom . '
' . $jour->format('d/m');
// --- 1) Dispos HEBDO ---
foreach ($dispos as $dispo) {
$dispoKey = normalize_day_key($dispo['jour_semaine'] ?? '');
if ($dispoKey !== $dayKey) {
continue;
}
// Parse HH:MM:SS
[$h1, $m1, $s1] = array_map('intval', explode(':', (string) $dispo['heure_debut']));
[$h2, $m2, $s2] = array_map('intval', explode(':', (string) $dispo['heure_fin']));
$debut_creneau = $jour->setTime($h1, $m1, $s1);
$fin_creneau = $jour->setTime($h2, $m2, $s2);
if ($fin_creneau <= $debut_creneau) {
continue;
}
for ($t = $debut_creneau; $t->add($step) <= $fin_creneau; $t = $t->add($step)) {
if ($t <= $now) {
continue;
}
$slotKey = $t->format('Y-m-d H:i');
if (! in_array($slotKey, $reservations, true)) {
$creneaux_par_jour[$colKey][$slotKey] = $t; // SET
}
}
}
// --- 2) Dispos PONCTUELLES (par date) ---
if (! empty($dispos_dates_by_day[$dateKey])) {
foreach ($dispos_dates_by_day[$dateKey] as $row) {
// Parse HH:MM:SS
[$h1, $m1, $s1] = array_map('intval', explode(':', (string) $row['heure_debut']));
[$h2, $m2, $s2] = array_map('intval', explode(':', (string) $row['heure_fin']));
$debut_creneau = $jour->setTime($h1, $m1, $s1);
$fin_creneau = $jour->setTime($h2, $m2, $s2);
if ($fin_creneau <= $debut_creneau) {
continue;
}
for ($t = $debut_creneau; $t->add($step) <= $fin_creneau; $t = $t->add($step)) {
if ($t <= $now) {
continue;
}
$slotKey = $t->format('Y-m-d H:i');
if (! in_array($slotKey, $reservations, true)) {
$creneaux_par_jour[$colKey][$slotKey] = $t; // SET + fusion
}
}
}
}
}
// Convertir SET -> liste triée
foreach ($creneaux_par_jour as $k => $set) {
$slots = array_values($set);
usort($slots, fn(DateTimeImmutable $a, DateTimeImmutable $b) => $a->getTimestamp() <=> $b->getTimestamp());
$creneaux_par_jour[$k] = $slots;
}
// Pagination des colonnes
$all_keys = array_keys($creneaux_par_jour);
$total_pages = (int) ceil(count($all_keys) / $limit_columns);
$offset = ($page - 1) * $limit_columns;
$visible_keys = array_slice($all_keys, $offset, $limit_columns);
require_once '../includes/header.php';
$PAGE_TITLE = $translations['creneaux_disponibles_title'] ?? 'Créneaux disponibles';
$HEAD_EXTRA = <<
HTML;
?>