319 lines
12 KiB
PHP
319 lines
12 KiB
PHP
<?php
|
||
require_once __DIR__ . '/../includes/init_client.php';
|
||
|
||
// Valeurs par défaut au cas où
|
||
if (! isset($c_online)) {$c_online = 'online';}
|
||
if (! isset($c_offline)) {$c_offline = 'offline';}
|
||
|
||
if (! isset($_GET['praticien_id'])) {
|
||
exit($translations['praticien_non_precise']);
|
||
}
|
||
|
||
$praticien_id = intval($_GET['praticien_id']);
|
||
$client_id = $_SESSION['reference_id'] ?? null;
|
||
$page = isset($_GET['page']) ? max(1, (int) $_GET['page']) : 1;
|
||
$limit_columns = 5;
|
||
|
||
// Infos praticien (+ adresse pour décider des radios)
|
||
$stmt = $pdo->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 . '<br> ' . $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
|
||
<script src="/js/sweetalert2@11.js"></script>
|
||
HTML;
|
||
?>
|
||
<div class="main1">
|
||
<div class="container py-5">
|
||
<div class="shadow p-4 rounded bg-light rdv-card">
|
||
<h3 class="rdv-subtitle"><?php echo htmlspecialchars(($praticien['prenom'] ?? '') . ' ' . ($praticien['nom'] ?? '')) ?></h3>
|
||
|
||
<?php if ($total_pages > 1): ?>
|
||
<div class="pagination">
|
||
<?php if ($page > 1): ?>
|
||
<a href="?praticien_id=<?php echo $praticien_id ?>&page=<?php echo $page - 1 ?>">« <?php echo $translations['pagination_precedent'] ?></a>
|
||
<?php endif; ?>
|
||
<?php if ($page < $total_pages): ?>
|
||
<a href="?praticien_id=<?php echo $praticien_id ?>&page=<?php echo $page + 1 ?>"><?php echo $translations['pagination_suivant'] ?> »</a>
|
||
<?php endif; ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<?php if (empty($creneaux_par_jour)): ?>
|
||
<p class="rdv-empty"><?php echo $translations['aucun_creneau_disponible_3_semaines'] ?><br><br>
|
||
<a class="btn btn-outline-secondary btn-sm btn-table" href="/messages/start.php?praticien_id=<?php echo $praticien_id ?>">
|
||
<?php echo $translations['chat_praticien'] ?>
|
||
</a></p>
|
||
<?php else: ?>
|
||
<center><a class="btn btn-outline-secondary btn-sm btn-table" href="/messages/start.php?praticien_id=<?php echo $praticien_id ?>">
|
||
<?php echo $translations['chat_praticien'] ?>
|
||
</a></center>
|
||
<div class="grid-container">
|
||
<?php foreach ($visible_keys as $jour): ?>
|
||
<div class="day-column">
|
||
<h4><?php echo $jour ?></h4>
|
||
<?php foreach ($creneaux_par_jour[$jour] as $c): ?>
|
||
<?php
|
||
$startIso = $c->format(DateTime::ATOM);
|
||
$slotName = 'mode_rdv_' . $c->format('YmdHi');
|
||
$onlineLabel = $translations['option_online'] ?? 'Онлайн';
|
||
$cabinetLabel = $translations['option_cabinet'] ?? 'В кабинете';
|
||
?>
|
||
<div class="slot">
|
||
<div class="slot-time"><?php echo $c->format('H:i') ?></div>
|
||
|
||
<?php if ($has_address): ?>
|
||
<div class="mode-group my-2">
|
||
<label class="me-3">
|
||
<input type="radio" name="<?php echo htmlspecialchars($slotName) ?>" value="<?php echo htmlspecialchars($c_online) ?>" checked>
|
||
<?php echo htmlspecialchars($onlineLabel) ?>
|
||
</label>
|
||
<label>
|
||
<input type="radio" name="<?php echo htmlspecialchars($slotName) ?>" value="<?php echo htmlspecialchars($c_offline) ?>">
|
||
<?php echo htmlspecialchars($cabinetLabel) ?>
|
||
</label>
|
||
</div>
|
||
<?php else: ?>
|
||
<div class="mode-group my-2">
|
||
<span><?php echo htmlspecialchars($onlineLabel) ?></span>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<a href="/public/client/valider_rdv.php?praticien_id=<?php echo $praticien_id ?>&start=<?php echo urlencode($startIso) ?>"
|
||
class="reserver-btn"
|
||
data-mode-name="<?php echo htmlspecialchars($has_address ? $slotName : '') ?>"
|
||
data-default-mode="<?php echo htmlspecialchars($c_online) ?>"
|
||
data-nom="<?php echo htmlspecialchars($praticien['nom'] ?? '') ?>"
|
||
data-prenom="<?php echo htmlspecialchars($praticien['prenom'] ?? '') ?>"
|
||
data-start="<?php echo htmlspecialchars($startIso) ?>">
|
||
<?php echo $translations['btn_reserver'] ?>
|
||
</a>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
document.querySelectorAll('.reserver-btn').forEach(btn => {
|
||
btn.addEventListener('click', function (e) {
|
||
e.preventDefault();
|
||
const start = this.dataset.start;
|
||
const nom = this.dataset.nom;
|
||
const prenom = this.dataset.prenom;
|
||
|
||
const groupName = this.dataset.modeName;
|
||
let modeVal = this.dataset.defaultMode || 'online';
|
||
if (groupName) {
|
||
const checked = this.closest('.slot').querySelector(`input[name="${groupName}"]:checked`);
|
||
if (checked) modeVal = checked.value;
|
||
}
|
||
|
||
const date = new Date(start);
|
||
const jour = date.toLocaleDateString('ru-RU', { weekday: 'long', day: '2-digit', month: '2-digit' });
|
||
const heure = date.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' });
|
||
|
||
Swal.fire({
|
||
title: <?php echo json_encode($translations['confirmation']) ?>,
|
||
html: `<b>${prenom} ${nom}<br>${jour} <?php echo $translations['a'] ?> ${heure}</b>`,
|
||
icon: 'question',
|
||
showCancelButton: true,
|
||
confirmButtonText: <?php echo json_encode($translations['btn_oui_reserver']) ?>,
|
||
cancelButtonText: <?php echo json_encode($translations['btn_annuler']) ?>
|
||
}).then((result) => {
|
||
if (result.isConfirmed) {
|
||
const url = new URL(this.href, location.origin);
|
||
url.searchParams.set('mode_rdv', modeVal);
|
||
window.location.href = url.toString();
|
||
}
|
||
});
|
||
});
|
||
});
|
||
</script>
|
||
</div>
|
||
<footer class="site-footer">
|
||
<div class="container">
|
||
<p><?php echo $translations['copyr'] ?></p>
|
||
</div>
|
||
</footer>
|
||
</body>
|
||
</html>
|