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

1): ?>



format(DateTime::ATOM); $slotName = 'mode_rdv_' . $c->format('YmdHi'); $onlineLabel = $translations['option_online'] ?? 'Онлайн'; $cabinetLabel = $translations['option_cabinet'] ?? 'В кабинете'; ?>
format('H:i') ?>