Files
maria_uslugi/public/client/reserver_rdv.php
T
2026-03-05 10:25:39 +01:00

319 lines
12 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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 ?>">&laquo; <?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'] ?> &raquo;</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>