Init Git Prod Mars 2026
This commit is contained in:
commit
d81efe93b1
362 files changed
+86627
No files matched your search
@@ -0,0 +1,297 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/../includes/header_cabinet.php';
|
||||
if (!function_exists('h')) { function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); } }
|
||||
|
||||
$token = trim((string)($_GET['token'] ?? ''));
|
||||
$inv = null;
|
||||
if ($token) {
|
||||
$q = $pdo->prepare("SELECT * FROM invitations WHERE token=:t AND status='envoyee' LIMIT 1");
|
||||
$q->execute(['t'=>$token]);
|
||||
$inv = $q->fetch(PDO::FETCH_ASSOC);
|
||||
}
|
||||
if (!$inv) {
|
||||
echo '<div class="container my-4"><div class="alert alert-danger">'.h($translations['inv_token_invalide'] ?? 'Lien invalide.').'</div></div>';
|
||||
exit;
|
||||
}
|
||||
if (!empty($inv['expires_at']) && new DateTime($inv['expires_at']) < new DateTime()) {
|
||||
echo '<div class="container my-4"><div class="alert alert-warning">'.h($translations['inv_token_expire'] ?? 'Lien expiré.').'</div></div>';
|
||||
exit;
|
||||
}
|
||||
|
||||
$email = strtolower((string)$inv['email']);
|
||||
$praticien_id = (int)$inv['praticien_id'];
|
||||
$connectedRole = strtolower((string)($_SESSION['role'] ?? ''));
|
||||
$connectedId = (int)($_SESSION['compte_id'] ?? 0);
|
||||
$connectedEmail= strtolower((string)($_SESSION['email'] ?? ''));
|
||||
|
||||
$done = false;
|
||||
$msg = '';
|
||||
|
||||
// =====================================================
|
||||
// CAS 1 : Déjà connecté comme secrétaire
|
||||
// =====================================================
|
||||
if ($connectedRole === 'secretaire' && $connectedId > 0) {
|
||||
if ($connectedEmail === $email) {
|
||||
// rattachement au praticien si pas encore lié
|
||||
$chk = $pdo->prepare("SELECT id FROM secretaire_praticien WHERE secretaire_user_id=:u AND praticien_id=:p");
|
||||
$chk->execute(['u'=>$connectedId,'p'=>$praticien_id]);
|
||||
if (!$chk->fetch()) {
|
||||
$ins = $pdo->prepare("INSERT INTO secretaire_praticien (secretaire_user_id, praticien_id, can_manage_rdv, can_edit_slots, is_active)
|
||||
VALUES (:u,:p,1,1,1)");
|
||||
$ins->execute(['u'=>$connectedId,'p'=>$praticien_id]);
|
||||
}
|
||||
$pdo->prepare("UPDATE invitations SET status='acceptee' WHERE id=:id")->execute(['id'=>$inv['id']]);
|
||||
$done = true;
|
||||
} else {
|
||||
// autre email → doit se connecter avec le bon compte
|
||||
$msg = sprintf(
|
||||
$translations['inv_email_diff'] ?? "Vous êtes connecté avec %s, mais l’invitation est pour %s. Veuillez vous connecter avec le bon compte.",
|
||||
h($connectedEmail), h($email)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// CAS 2 : POST (création compte si inexistant)
|
||||
// =====================================================
|
||||
elseif (($_SERVER['REQUEST_METHOD']??'')==='POST') {
|
||||
$pwd = (string)($_POST['pwd'] ?? '');
|
||||
$pwd2 = (string)($_POST['pwd2'] ?? '');
|
||||
|
||||
if (strlen($pwd) < 8 || $pwd !== $pwd2) {
|
||||
$msg = $translations['inv_pwd_invalid'] ?? 'Mot de passe invalide ou non identique.';
|
||||
} else {
|
||||
// Vérifier si un compte existe déjà avec ce mail
|
||||
$chk = $pdo->prepare("SELECT id, role FROM compte WHERE email=:e LIMIT 1");
|
||||
$chk->execute(['e'=>$email]);
|
||||
$acc = $chk->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($acc) {
|
||||
if ($acc['role'] !== 'secretaire') {
|
||||
$msg = $translations['inv_email_occupe'] ?? "Un compte avec cet email existe déjà avec un autre rôle.";
|
||||
} else {
|
||||
// compte secrétaire déjà existant → rediriger vers login AVEC token
|
||||
header("Location: /connexion.php?next=" . urlencode("/public/cabinet/accepter_invitation.php?token=".$token));
|
||||
exit;
|
||||
}
|
||||
} else {
|
||||
// création nouveau compte secrétaire
|
||||
$ins = $pdo->prepare("INSERT INTO compte (email, mot_de_passe, role, reference_id, token_validation, email_valide)
|
||||
VALUES (:e, :pwd, 'secretaire', 0, NULL, 1)");
|
||||
$ins->execute([
|
||||
'e' => $email,
|
||||
'pwd' => password_hash($pwd, PASSWORD_BCRYPT)
|
||||
]);
|
||||
$newId = (int)$pdo->lastInsertId();
|
||||
|
||||
$_SESSION['compte_id'] = $newId;
|
||||
$_SESSION['role'] = 'secretaire';
|
||||
$_SESSION['reference_id'] = 0;
|
||||
$_SESSION['email'] = $email;
|
||||
|
||||
$pdo->prepare("INSERT INTO secretaire_praticien (secretaire_user_id, praticien_id, can_manage_rdv, can_edit_slots, is_active)
|
||||
VALUES (:u,:p,1,1,1)")
|
||||
->execute(['u'=>$newId,'p'=>$praticien_id]);
|
||||
|
||||
$pdo->prepare("UPDATE invitations SET status='acceptee' WHERE id=:id")->execute(['id'=>$inv['id']]);
|
||||
$done = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// CAS 3 : GET initial (ni connecté ni posté)
|
||||
// Vérifier si compte existe déjà
|
||||
// =====================================================
|
||||
else {
|
||||
$chk = $pdo->prepare("SELECT id, role FROM compte WHERE email=:e LIMIT 1");
|
||||
$chk->execute(['e'=>$email]);
|
||||
$acc = $chk->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($acc && $acc['role']==='secretaire') {
|
||||
echo '<div class="container my-4"><div class="alert alert-info">'.
|
||||
h($translations['inv_compte_existe'] ?? 'Un compte secrétaire existe déjà, veuillez vous connecter.').
|
||||
'</div><a href="/connexion.php?next='.urlencode("/public/cabinet/accepter_invitation.php?token=".$token).'" class="btn btn-primary">Connexion</a></div>';
|
||||
exit;
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="main1">
|
||||
<div class="container py-5">
|
||||
<div class="shadow p-4 rounded bg-light mx-auto" style="max-width:600px;">
|
||||
<h1 class="h4 mb-3"><?= h($translations['inv_accepter_titre'] ?? 'Accepter l’invitation') ?></h1>
|
||||
|
||||
<?php if($done): ?>
|
||||
<div class="alert alert-success mb-4"><?= h($translations['inv_ok'] ?? 'Invitation acceptée. Vous avez désormais accès au secrétariat.') ?></div>
|
||||
<a class="btn btn-primary" href="/public/cabinet/dashboard.php"><?= h($translations['btn_aller_dashboard_sec'] ?? 'Aller au tableau de bord secrétaire') ?></a>
|
||||
<?php else: ?>
|
||||
|
||||
<div class="card mb-3"><div class="card-body">
|
||||
<p class="mb-1"><strong><?= h($translations['inv_email'] ?? 'Email') ?>:</strong> <?= h($email) ?></p>
|
||||
<p class="mb-0"><strong><?= h($translations['inv_praticien'] ?? 'Praticien') ?>:</strong> #<?= (int)$praticien_id ?></p>
|
||||
</div></div>
|
||||
|
||||
<?php if(!empty($msg)): ?>
|
||||
<div class="alert alert-warning"><?= $msg ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if($connectedRole!=='secretaire' || $connectedEmail!==$email): ?>
|
||||
<form method="post" class="card p-3" id="create-secretary-form">
|
||||
<label class="form-label mb-2">
|
||||
<?= h($translations['inv_choisir_pwd'] ?? 'Choisissez un mot de passe (min. 8 caractères)') ?>
|
||||
</label>
|
||||
|
||||
<!-- Mot de passe -->
|
||||
<!-- MDP -->
|
||||
<div class="mb-2 position-relative">
|
||||
<input type="password" name="pwd" id="pwd" class="form-control pe-5"
|
||||
minlength="8" required autocomplete="new-password"
|
||||
placeholder="<?= h($translations['motdepasse_placeholder'] ?? 'Mot de passe') ?>">
|
||||
<span class="toggle-password" onclick="togglePassword('pwd')"
|
||||
style="position:absolute;right:12px;top:50%;transform:translateY(-50%);cursor:pointer;user-select:none;">👁️</span>
|
||||
</div>
|
||||
|
||||
<div id="niveau-securite" class="mb-2 text-muted">
|
||||
<?= h($translations['mdp_regle_min'] ?? 'min 8: (A–z)+(1–9)+(&–!)') ?>
|
||||
</div>
|
||||
|
||||
<div class="strength-bar d-flex gap-1 mb-3" aria-hidden="true" style="height:8px;">
|
||||
<div class="strength-fill flex-fill" style="background:#d9d9d9;border-radius:6px;"></div>
|
||||
<div class="strength-fill flex-fill" style="background:#d9d9d9;border-radius:6px;"></div>
|
||||
<div class="strength-fill flex-fill" style="background:#d9d9d9;border-radius:6px;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Confirmation -->
|
||||
<div class="mb-2 position-relative">
|
||||
<input type="password" name="pwd2" id="pwd2" class="form-control pe-5"
|
||||
minlength="8" required autocomplete="new-password"
|
||||
placeholder="<?= h($translations['confirm_mdp_placeholder'] ?? 'Confirmer le mot de passe') ?>">
|
||||
<span class="toggle-password" onclick="togglePassword('pwd2')"
|
||||
style="position:absolute;right:12px;top:50%;transform:translateY(-50%);cursor:pointer;user-select:none;">👁️</span>
|
||||
</div>
|
||||
<small id="confirm-feedback" class="d-block mb-3 text-muted" style="display:none;">—</small>
|
||||
|
||||
<button id="btn-submit" class="btn btn-primary" disabled>
|
||||
<?= h($translations['inv_btn_creer_compte'] ?? 'Créer mon compte & accepter') ?>
|
||||
</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p><?= $translations['copyr'] ?? '© '.date('Y') ?></p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
<script>
|
||||
function togglePassword(id){
|
||||
const i = document.getElementById(id);
|
||||
if (i) i.type = (i.type === 'password') ? 'text' : 'password';
|
||||
}
|
||||
|
||||
(function(){
|
||||
const form = document.getElementById('create-secretary-form') || document.getElementById('form-inscription') || document.forms[0];
|
||||
const pwd = document.getElementById('pwd');
|
||||
const cpwd = document.getElementById('pwd2');
|
||||
const fills = Array.from(document.querySelectorAll('.strength-fill'));
|
||||
const hint = document.getElementById('niveau-securite');
|
||||
const fb = document.getElementById('confirm-feedback');
|
||||
const submit = document.getElementById('btn-submit');
|
||||
|
||||
const T = {
|
||||
rule : '<?= addslashes($translations['mdp_regle_min'] ?? 'min 8: (A–z)+(1–9)+(&–!)') ?>',
|
||||
weak : '<?= addslashes($translations['securite_faible'] ?? 'Sécurité : faible') ?>',
|
||||
medium: '<?= addslashes($translations['securite_moyen'] ?? 'Sécurité : moyen') ?>',
|
||||
strong: '<?= addslashes($translations['securite_fort'] ?? 'Sécurité : fort') ?>',
|
||||
ok : '<?= addslashes($translations['mdp_identiques_ok'] ?? 'Les mots de passe correspondent.') ?>',
|
||||
ko : '<?= addslashes($translations['mdp_non_identiques'] ?? 'Les mots de passe ne correspondent pas.') ?>'
|
||||
};
|
||||
|
||||
function score(val){
|
||||
if(!val) return 0;
|
||||
let s = 0;
|
||||
if(/[a-z]/.test(val)) s++;
|
||||
if(/[A-Z]/.test(val)) s++;
|
||||
if(/\d/.test(val)) s++;
|
||||
if(/[\W_]/.test(val)) s++;
|
||||
if(val.length>=8) s++;
|
||||
return Math.min(s,5); // 0..5
|
||||
}
|
||||
|
||||
function paintStrength(s){
|
||||
// reset
|
||||
fills.forEach(el => el.style.background = '#d9d9d9');
|
||||
// niveaux: 0-1 faible(1 segment), 2-3 moyen(2), 4-5 fort(3)
|
||||
let n = 0, color = '#dc3545'; // rouge
|
||||
if (s <= 1) { n = 1; color = '#dc3545'; }
|
||||
else if (s <= 3) { n = 2; color = '#ffc107'; } // jaune
|
||||
else { n = 3; color = '#28a745'; } // vert
|
||||
for (let i=0;i<n;i++) fills[i].style.background = color;
|
||||
|
||||
if (hint){
|
||||
hint.classList.remove('text-danger','text-warning','text-success');
|
||||
if (n===1){ hint.textContent = T.weak; hint.classList.add('text-danger'); }
|
||||
else if (n===2){ hint.textContent = T.medium; hint.classList.add('text-warning'); }
|
||||
else { hint.textContent = T.strong; hint.classList.add('text-success'); }
|
||||
}
|
||||
}
|
||||
|
||||
function checkMatch(){
|
||||
if (!fb) return false;
|
||||
if (!cpwd.value){ fb.style.display='none'; return false; }
|
||||
const same = (pwd.value === cpwd.value) && cpwd.value.length>=8;
|
||||
fb.textContent = same ? T.ok : T.ko;
|
||||
fb.style.color = same ? '#198754' : '#dc3545';
|
||||
fb.style.display = 'block';
|
||||
return same;
|
||||
}
|
||||
|
||||
function update(){
|
||||
const v = pwd.value;
|
||||
|
||||
// ——— ÉTAT NEUTRE TANT QUE RIEN N'EST SAISI ———
|
||||
if (!v) {
|
||||
// barre toute grise
|
||||
fills.forEach(el => el.style.background = '#d9d9d9');
|
||||
// texte d'aide (règle) en gris
|
||||
if (hint){
|
||||
hint.textContent = T.rule;
|
||||
hint.classList.remove('text-danger','text-warning','text-success');
|
||||
hint.classList.add('text-muted');
|
||||
}
|
||||
// reset confirmation
|
||||
if (fb){ fb.style.display = 'none'; }
|
||||
// bouton désactivé
|
||||
if (submit){ submit.disabled = true; }
|
||||
return; // <<< on sort ici, pas de rouge par défaut
|
||||
}
|
||||
|
||||
// ——— LOGIQUE NORMALE SI QUELQUE CHOSE EST SAISI ———
|
||||
const sc = score(v);
|
||||
paintStrength(sc);
|
||||
const same = checkMatch();
|
||||
|
||||
if (submit){
|
||||
// OK si force >= moyen (>=2 segments), longueur >=8 et confirmation OK
|
||||
const ok = (sc >= 2) && (v.length >= 8) && same;
|
||||
submit.disabled = !ok;
|
||||
}
|
||||
}
|
||||
|
||||
if (hint) hint.classList.remove('text-muted');
|
||||
if (pwd) pwd.addEventListener('input', update);
|
||||
if (cpwd) cpwd.addEventListener('input', update);
|
||||
if (form) form.addEventListener('submit', function(e){
|
||||
if (submit && submit.disabled){ e.preventDefault(); }
|
||||
});
|
||||
|
||||
update(); // init
|
||||
})();
|
||||
</script>
|
||||
@@ -0,0 +1,216 @@
|
||||
<?php
|
||||
// /public/cabinet/agenda.php
|
||||
require_once __DIR__ . '/../includes/init_secretaire.php';
|
||||
|
||||
|
||||
|
||||
if (!function_exists('h')) { function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); } }
|
||||
|
||||
$compteId = (int)($_SESSION['compte_id'] ?? 0);
|
||||
$lang = $_SESSION['lang'] ?? 'fr'; // 'fr' ou 'ru'
|
||||
$fcLocale = ($lang === 'ru') ? 'ru' : 'fr';
|
||||
|
||||
// --- on attend ?praticien_id=... (ex. produit par le dashboard secrétaire)
|
||||
$praticien_id = isset($_GET['praticien_id']) ? (int)$_GET['praticien_id'] : 0;
|
||||
if ($compteId <= 0 || $praticien_id <= 0) { http_response_code(403); exit('Forbidden'); }
|
||||
|
||||
// --- vérification pivot secrétaire -> praticien
|
||||
$st = $pdo->prepare("
|
||||
SELECT can_manage_rdv
|
||||
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) { http_response_code(403); exit('Forbidden'); }
|
||||
|
||||
// petit helper perm (même si on n’édite rien ici)
|
||||
$canManage = (int)($pivot['can_manage_rdv'] ?? 0) === 1;
|
||||
|
||||
|
||||
$PAGE_TITLE = $translations['titre_agenda_page'] ?? 'Agenda';
|
||||
$HEAD_EXTRA = <<<HTML
|
||||
<!-- FullCalendar -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.8/index.global.min.css" rel="stylesheet" />
|
||||
<script src="/js/index.global.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.8/locales-all.global.min.js"></script>
|
||||
|
||||
<!-- SweetAlert2 -->
|
||||
<script src="/js/sweetalert2@11.js"></script>
|
||||
<link href="/js/sweetalert2.min.css" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
:root{
|
||||
--card-bg:#fff; --muted:#6c757d; --brand:#38378B; --accent:#e9ecef; --text:#222; --border:#e9ecef;
|
||||
}
|
||||
body{ background:#8CA9FF; color:var(--text); }
|
||||
.agenda-wrap{ max-width:1100px; margin:clamp(8px, 2vw, 24px) auto; padding:clamp(8px, 2vw, 24px); }
|
||||
.agenda-card{ background:var(--card-bg); border:1px solid var(--border); border-radius:16px; box-shadow:0 6px 18px rgba(0,0,0,.06); padding:clamp(12px,2.4vw,28px); }
|
||||
.agenda-title{ margin:0 0 12px; text-align:center; font-weight:700; letter-spacing:.2px; }
|
||||
#calendar{ min-height:420px; }
|
||||
.fc .fc-toolbar.fc-header-toolbar{ gap:8px; flex-wrap:wrap; }
|
||||
.fc .fc-toolbar-title{ font-size:clamp(18px, 2.4vw, 22px); color:var(--brand); }
|
||||
.fc .fc-button{ border-radius:999px; border:1px solid var(--border); background:#fff; color:#111; box-shadow:0 2px 6px rgba(0,0,0,.05); padding:6px 10px; }
|
||||
.fc .fc-button-primary:not(:disabled).fc-button-active, .fc .fc-button-primary:not(:disabled):active, .fc .fc-button-primary:hover{
|
||||
background:var(--accent); border-color:transparent; color:#000;
|
||||
}
|
||||
.fc .fc-col-header-cell-cushion{ color: var(--brand); font-weight:600; }
|
||||
.fc .fc-timegrid-slot-label, .fc .fc-timegrid-axis-cushion{ color:var(--muted); }
|
||||
.fc .fc-event{ border-radius:12px; border:0; padding:2px 4px; }
|
||||
.fc .fc-daygrid-event{ padding:2px 6px; }
|
||||
.empty-state{ text-align:center; color:var(--muted); padding:28px 12px 8px; font-size:15px; }
|
||||
.fc .fc-timegrid-now-indicator-line{ border-color:#ff5a5f; }
|
||||
@media (max-width: 768px){
|
||||
.agenda-card{ max-width: 720px; border-radius:14px; }
|
||||
.fc .fc-toolbar.fc-header-toolbar{ justify-content:center; }
|
||||
}
|
||||
</style>
|
||||
HTML;
|
||||
|
||||
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['raspisanie'] ?? 'Agenda') ?></h2>
|
||||
<div id="calendar" role="application" aria-label="calendar"></div>
|
||||
<div id="emptyMsg" class="empty-state" style="display:none;">
|
||||
<?= h($translations['agenda_aucun_evenement'] ?? 'Aucun rendez-vous à afficher.') ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p><?= $translations['copyr'] ?? '© '.date('Y') ?></p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const calendarEl = document.getElementById('calendar');
|
||||
const emptyMsgEl = document.getElementById('emptyMsg');
|
||||
|
||||
const t = <?= json_encode([
|
||||
'rdv_avec' => $translations['sw_rdv_avec'] ?? 'RDV avec',
|
||||
'date' => $translations['sw_date'] ?? 'Date',
|
||||
'heure' => $translations['sw_heure'] ?? 'Heure',
|
||||
'statut' => $translations['sw_statut'] ?? 'Statut',
|
||||
'gerer_rdv' => $translations['sw_gerer_rdv'] ?? 'Gérer le RDV',
|
||||
'btn_fermer' => $translations['sw_btn_fermer'] ?? 'Fermer',
|
||||
'erreur' => $translations['erreur_chargement_rdv'] ?? 'Erreur de chargement',
|
||||
'statut_confirme' => $translations['statut_confirme'] ?? 'Confirmé',
|
||||
'statut_prevu' => $translations['statut_prevu'] ?? 'Prévu',
|
||||
'statut_annule' => $translations['statut_annule'] ?? 'Annulé',
|
||||
], JSON_UNESCAPED_UNICODE) ?>;
|
||||
|
||||
const fcLocale = <?= json_encode($fcLocale) ?>;
|
||||
const praticienId = <?= (int)$praticien_id ?>;
|
||||
|
||||
const isMobile = window.matchMedia('(max-width: 768px)').matches;
|
||||
const initialView = isMobile ? 'listWeek' : 'timeGridWeek';
|
||||
|
||||
const calendar = new window.FullCalendar.Calendar(calendarEl, {
|
||||
locale: fcLocale,
|
||||
timeZone: 'Europe/Paris',
|
||||
initialView,
|
||||
nowIndicator: true,
|
||||
stickyHeaderDates: true,
|
||||
expandRows: true,
|
||||
slotMinTime: "07:00:00",
|
||||
slotMaxTime: "21:00:00",
|
||||
firstDay: 1,
|
||||
height: isMobile ? 'auto' : 680,
|
||||
customButtons: {
|
||||
btnJour: { text: '<?= addslashes($translations['btn_jour'] ?? 'Jour') ?>', click(){ calendar.changeView('listDay'); } },
|
||||
btnSemaine: { text: '<?= addslashes($translations['btn_semaine'] ?? 'Semaine') ?>', click(){ calendar.changeView('listWeek'); } }
|
||||
},
|
||||
headerToolbar: {
|
||||
left: 'prev,next,today',
|
||||
center: 'title',
|
||||
right: isMobile ? 'btnJour,btnSemaine,dayGridMonth' : 'timeGridWeek,dayGridMonth'
|
||||
},
|
||||
buttonText: {
|
||||
today: '<?= addslashes($translations['btn_auj'] ?? "Aujourd'hui") ?>',
|
||||
month: '<?= addslashes($translations['btn_mois'] ?? 'Mois') ?>',
|
||||
week: '<?= addslashes($translations['btn_semaine'] ?? 'Semaine') ?>',
|
||||
day: '<?= addslashes($translations['btn_jour'] ?? 'Jour') ?>',
|
||||
list: '<?= addslashes($translations['btn_liste'] ?? 'Liste') ?>'
|
||||
},
|
||||
views: { listDay:{buttonText:'<?= addslashes($translations['btn_jour'] ?? 'Jour') ?>'},
|
||||
listWeek:{buttonText:'<?= addslashes($translations['btn_semaine'] ?? 'Semaine') ?>'} },
|
||||
events: {
|
||||
url: '/public/cabinet/events.php',
|
||||
method: 'POST',
|
||||
extraParams: { praticien_id: praticienId },
|
||||
failure: function () {
|
||||
Swal.fire({ icon: 'error', title: 'Oops…', text: t.erreur });
|
||||
}
|
||||
},
|
||||
loading: function(isLoading) {
|
||||
if (isLoading) return;
|
||||
emptyMsgEl.style.display = calendar.getEvents().length > 0 ? 'none' : 'block';
|
||||
},
|
||||
eventContent: function(arg){
|
||||
if (arg.view.type.includes('timeGrid') || arg.view.type.includes('dayGrid')) {
|
||||
const title = document.createElement('div');
|
||||
title.style.fontWeight = '600';
|
||||
title.style.fontSize = '12px';
|
||||
title.textContent = arg.event.title;
|
||||
|
||||
const time = document.createElement('div');
|
||||
time.style.fontSize = '11px';
|
||||
time.style.opacity = '.9';
|
||||
time.textContent = arg.timeText;
|
||||
|
||||
return { domNodes: [title, time] };
|
||||
}
|
||||
return true;
|
||||
},
|
||||
eventClick: function(info) {
|
||||
const start = new Date(info.event.start);
|
||||
const end = new Date(info.event.end);
|
||||
|
||||
const dateStr = start.toLocaleDateString(<?= json_encode($lang === 'ru' ? 'ru-RU' : 'fr-FR') ?>, { weekday:'long', year:'numeric', month:'long', day:'numeric' });
|
||||
const heureDebut = start.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
const heureFin = end.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
|
||||
const raw = (info.event.extendedProps.statut || '').trim().toLowerCase();
|
||||
const fr2key = { 'prévu': 'planned', 'confirmé': 'confirmed', 'annulé': 'cancelled' };
|
||||
const key = fr2key[raw] || raw;
|
||||
|
||||
const statutMap = { planned: t.statut_prevu, confirmed: t.statut_confirme, cancelled: t.statut_annule };
|
||||
const statutLabel = statutMap[key] ?? key;
|
||||
|
||||
Swal.fire({
|
||||
title: `${t.rdv_avec} ${info.event.title}`,
|
||||
html: `
|
||||
<p><strong>${t.date} :</strong> ${dateStr}</p>
|
||||
<p><strong>${t.heure} :</strong> ${heureDebut} – ${heureFin}</p>
|
||||
<p><strong>${t.statut} :</strong> ${statutLabel}</p>
|
||||
<a class="swal2-confirm swal2-styled" style="display:inline-block;margin-top:8px;border-radius:999px"
|
||||
href="/public/cabinet/rendez_vous.php?id=${info.event.id}&praticien_id=${praticienId}" target="_blank">
|
||||
${t.gerer_rdv}
|
||||
</a>
|
||||
`,
|
||||
showConfirmButton: true,
|
||||
confirmButtonText: t.btn_fermer
|
||||
});
|
||||
},
|
||||
eventDidMount: function(info){ info.el.setAttribute('aria-label', info.event.title); },
|
||||
windowResize: function(){
|
||||
const mobile = window.matchMedia('(max-width: 768px)').matches;
|
||||
calendar.setOption('height', mobile ? 'auto' : 680);
|
||||
if (mobile && !calendar.view.type.startsWith('list')) calendar.changeView('listWeek');
|
||||
}
|
||||
});
|
||||
|
||||
calendar.render();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,685 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../includes/init_secretaire.php';
|
||||
|
||||
|
||||
require_once APP_HTDOCS . '/push/push_endpoint.php';
|
||||
|
||||
|
||||
// PHPMailer (config centralisée)
|
||||
|
||||
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use PHPMailer\PHPMailer\Exception;
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
// ------- helpers JSON -------
|
||||
function out(bool $ok, string $message, array $extra = []): never {
|
||||
echo json_encode(['ok'=>$ok, 'message'=>$message] + $extra, JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ------- helpers e-mail HTML/TXT -------
|
||||
function email_html_status(array $d, array $t): string {
|
||||
$brand = '<img src="https://uslugi.fr/public/uploads/img/logo_mail.png" width="150" alt="USLUGI.FR">';
|
||||
$brand_bas ='УСЛУГИ.ФР';
|
||||
return '<!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>USLUGI.FR</title></head>
|
||||
<body style="margin:0;padding:0;background:#f4f6f8;font-family:Arial,Helvetica,sans-serif;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f4f6f8;padding:24px 0;"><tr><td align="center">
|
||||
<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="background:#fff;border-radius:8px;overflow:hidden;">
|
||||
<tr><td style="background:#38378B;color:#fff;padding:20px 24px;font-size:18px;font-weight:600;">'.$brand.'</td></tr>
|
||||
<tr><td style="padding:24px 24px 8px 24px;color:#111;font-size:16px;line-height:1.5;">'.nl2br(htmlspecialchars($d['hello_line'])).'</td></tr>
|
||||
<tr><td style="padding:8px 24px;color:#111;font-size:16px;line-height:1.6;">'.nl2br(htmlspecialchars($d['intro'])).'</td></tr>
|
||||
<tr><td style="padding:16px 24px;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border:1px solid #e5e7eb;border-radius:8px;">
|
||||
<tr><td style="padding:14px 16px;font-size:14px;color:#6b7280;border-bottom:1px solid #e5e7eb;">'.htmlspecialchars($d['who_label']).'</td><td style="padding:14px 16px;font-size:14px;color:#111;">'.htmlspecialchars($d['who_value']).'</td></tr>
|
||||
<tr><td style="padding:14px 16px;font-size:14px;color:#6b7280;">'.htmlspecialchars($d['date_label']).'</td><td style="padding:14px 16px;font-size:14px;color:#111;">'.htmlspecialchars($d['when']).'</td></tr>
|
||||
<tr><td style="padding:14px 16px;font-size:14px;color:#6b7280;">'.htmlspecialchars($d['statut_label']).'</td><td style="padding:14px 16px;font-size:14px;color:#111;">'.htmlspecialchars($d['statut_value']).'</td></tr>
|
||||
</table>
|
||||
</td></tr>'.
|
||||
(!empty($d['cta_url']) ? '<tr><td align="center" style="padding:8px 24px 24px 24px;"><a href="'.htmlspecialchars($d['cta_url']).'" target="_blank" style="display:inline-block;text-decoration:none;padding:12px 20px;border-radius:999px;background:#E03C3F;color:#fff;font-weight:600;">'.htmlspecialchars($d['cta_label']).'</a></td></tr>' : '').
|
||||
'<tr><td style="padding:8px 24px 24px 24px;color:#111;font-size:16px;line-height:1.6;">'.nl2br(htmlspecialchars($d['footer'])).'</td></tr>
|
||||
</table>
|
||||
<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="margin-top:12px;"><tr><td style="text-align:center;color:#9ca3af;font-size:12px;padding:8px 0;">© '.date('Y').' '.$brand_bas.' — '.htmlspecialchars($t['merci_simple'] ?? 'Merci').'</td></tr></table>
|
||||
</td></tr></table></body></html>';
|
||||
}
|
||||
function email_text_status(array $d, array $t): string {
|
||||
$L=[]; $L[]=$d['hello_line']; $L[]=''; $L[]=$d['intro']; $L[]='';
|
||||
$L[]=$d['who_label'].': '.$d['who_value']; $L[]=$d['date_label'].': '.$d['when']; $L[]=$d['statut_label'].': '.$d['statut_value'];
|
||||
if (!empty($d['cta_url'])) { $L[]=''; $L[] = $d['cta_label'].': '.$d['cta_url']; }
|
||||
$L[]=''; $L[]=$d['footer']; return implode("\n",$L);
|
||||
}
|
||||
|
||||
// ------- ICS helpers -------
|
||||
function ics_escape(string $s): string {
|
||||
$s = str_replace("\\","\\\\",$s);
|
||||
$s = str_replace(";","\;",$s);
|
||||
$s = str_replace(",","\,",$s);
|
||||
return preg_replace("/\r\n|\r|\n/","\\n",$s);
|
||||
}
|
||||
function ics_build(
|
||||
string $uid, DateTime $start, DateTime $end, string $tz, string $summary, string $desc, string $loc,
|
||||
array $attendees, string $method='REQUEST', string $status='CONFIRMED'
|
||||
): string {
|
||||
$L=[];
|
||||
$L[]="BEGIN:VCALENDAR";
|
||||
$L[]="VERSION:2.0";
|
||||
$L[]="PRODID:-//УСЛУГИ.ФР//RDV//FR";
|
||||
$L[]="CALSCALE:GREGORIAN";
|
||||
$L[]="METHOD:{$method}";
|
||||
$L[]="BEGIN:VEVENT";
|
||||
$L[]="UID:{$uid}";
|
||||
$L[]="DTSTAMP:".gmdate('Ymd\THis\Z');
|
||||
$L[]="DTSTART;TZID={$tz}:".$start->format('Ymd\THis');
|
||||
$L[]="DTEND;TZID={$tz}:".$end->format('Ymd\THis');
|
||||
if ($summary) $L[]="SUMMARY:".ics_escape($summary);
|
||||
if ($desc) $L[]="DESCRIPTION:".ics_escape($desc);
|
||||
if ($loc) $L[]="LOCATION:".ics_escape($loc);
|
||||
$L[]="ORGANIZER;CN=УСЛУГИ.ФР:mailto:rdv@uslugi.fr";
|
||||
foreach ($attendees as $a) {
|
||||
if (empty($a['email'])) continue;
|
||||
$cn = ics_escape($a['cn'] ?? '');
|
||||
$L[]="ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;CN={$cn};RSVP=TRUE:mailto:".$a['email'];
|
||||
}
|
||||
$L[]="STATUS:{$status}";
|
||||
if ($method==='CANCEL') $L[]="SEQUENCE:2";
|
||||
$L[]="END:VEVENT";
|
||||
$L[]="END:VCALENDAR";
|
||||
return implode("\r\n",$L)."\r\n";
|
||||
}
|
||||
|
||||
// ------- SMTP send (PHPMailer) -------
|
||||
function smtp_send(
|
||||
string $to, string $subject, string $text, string $html,
|
||||
?string $icsName=null, ?string $icsContent=null, ?string $icsMethod=null
|
||||
): array {
|
||||
global $MAILCFG;
|
||||
$m = new PHPMailer(true);
|
||||
try {
|
||||
$m->isSMTP();
|
||||
$m->Host = $MAILCFG['smtp']['host'];
|
||||
$m->Port = $MAILCFG['smtp']['port'];
|
||||
$m->SMTPSecure = $MAILCFG['smtp']['secure'];
|
||||
$m->SMTPAuth = $MAILCFG['smtp']['auth'];
|
||||
$m->Username = $MAILCFG['smtp']['username'];
|
||||
$m->Password = $MAILCFG['smtp']['password'];
|
||||
$m->CharSet = 'UTF-8';
|
||||
$m->Encoding = 'base64';
|
||||
$m->XMailer = '';
|
||||
|
||||
if (!empty($_GET['debug_mail'])) {
|
||||
$m->SMTPDebug = 2;
|
||||
$m->Debugoutput = function($s,$lvl){ error_log("SMTP[$lvl] ".$s); };
|
||||
}
|
||||
|
||||
if (!empty($MAILCFG['bounce'])) $m->Sender = $MAILCFG['bounce'];
|
||||
$from = $MAILCFG['from'] ?? ['no-reply@uslugi.fr','USLUGI.FR'];
|
||||
$rpto = $MAILCFG['reply_to'] ?? ['contact@uslugi.fr','USLUGI.FR'];
|
||||
$m->setFrom($from[0], $from[1] ?? '');
|
||||
if (!empty($rpto[0])) $m->addReplyTo($rpto[0], $rpto[1] ?? '');
|
||||
|
||||
$m->addAddress($to);
|
||||
if (!empty($MAILCFG['suppress_auto'])) {
|
||||
$m->addCustomHeader('Auto-Submitted', 'auto-generated');
|
||||
$m->addCustomHeader('X-Auto-Response-Suppress', 'All');
|
||||
}
|
||||
|
||||
$m->isHTML(true);
|
||||
$m->Subject = $subject;
|
||||
$m->Body = $html;
|
||||
$m->AltBody = $text;
|
||||
|
||||
if ($icsName && $icsContent !== null && $icsMethod) {
|
||||
$m->addStringAttachment($icsContent, $icsName, 'base64', 'text/calendar; method='.$icsMethod.'; charset=UTF-8');
|
||||
}
|
||||
|
||||
$m->send();
|
||||
return [true, null];
|
||||
|
||||
} catch (Throwable $e) {
|
||||
error_log('PHPMailer changer_statut (cabinet) error: '.$e->getMessage().($m->ErrorInfo ? ' | '.$m->ErrorInfo : ''));
|
||||
// retry sans ICS
|
||||
if ($icsName && $icsContent !== null && $icsMethod) {
|
||||
try { $m->clearAttachments(); $m->send(); return [true, 'sent_without_ics']; }
|
||||
catch(Throwable $e2){ error_log('PHPMailer fallback: '.$e2->getMessage()); }
|
||||
}
|
||||
return [false, $e->getMessage()];
|
||||
}
|
||||
}
|
||||
// ===== Badge helpers =====
|
||||
function get_compte_id(PDO $pdo, string $role, int $reference_id): int {
|
||||
$st = $pdo->prepare("SELECT id FROM compte WHERE role = ? AND reference_id = ? LIMIT 1");
|
||||
$st->execute([$role, $reference_id]);
|
||||
return (int)($st->fetchColumn() ?: 0);
|
||||
}
|
||||
|
||||
function badge_rdv_inc(PDO $pdo, int $compte_id, int $delta = 1): void {
|
||||
if ($compte_id <= 0) return;
|
||||
$pdo->prepare("
|
||||
INSERT INTO push_badge_counters (compte_id, msg_unread, rdv_unread, updated_at)
|
||||
VALUES (:cid, 0, :d, NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
rdv_unread = LEAST(99, GREATEST(0, rdv_unread + :d)),
|
||||
updated_at = NOW()
|
||||
")->execute([':cid' => $compte_id, ':d' => $delta]);
|
||||
}
|
||||
|
||||
// =================== Contrôles d’entrée ===================
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') out(false, 'Méthode invalide.');
|
||||
|
||||
$compteId = (int)($_SESSION['compte_id'] ?? 0);
|
||||
$userRole = strtolower((string)($_SESSION['role'] ?? ''));
|
||||
$praticien_id = (int)($_POST['praticien_id'] ?? 0); // <<< IMPORTANT côté secrétaire
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$statut = (string)($_POST['statut'] ?? '');
|
||||
|
||||
$whitelist = ['prévu','confirmé','annulé','terminé'];
|
||||
if ($userRole !== 'secretaire' || $compteId<=0 || $praticien_id<=0 || $id<=0 || !in_array($statut, $whitelist, true)) {
|
||||
out(false, $translations['erreur_mise_a_jour'] ?? 'Erreur de mise à jour.');
|
||||
}
|
||||
|
||||
// =================== Vérif pivot secrétaire ===================
|
||||
$st = $pdo->prepare("
|
||||
SELECT can_manage_rdv
|
||||
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_manage_rdv'] !== 1) {
|
||||
out(false, 'Forbidden');
|
||||
}
|
||||
|
||||
// =================== Update DB sécurisé ===================
|
||||
try {
|
||||
// =========================
|
||||
// SNAPSHOT AVANT UPDATE (secrétaire)
|
||||
// =========================
|
||||
$beforeStmt = $pdo->prepare("
|
||||
SELECT statut, date_rdv, duree_minutes, lieu, praticien_id, client_id
|
||||
FROM rendez_vous
|
||||
WHERE id = :id AND praticien_id = :pid
|
||||
LIMIT 1
|
||||
");
|
||||
$beforeStmt->execute([':id'=>$id, ':pid'=>$praticien_id]);
|
||||
$beforeRow = $beforeStmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$beforeRow) {
|
||||
out(false, $translations['erreur_mise_a_jour'] ?? 'Erreur de mise à jour.');
|
||||
}
|
||||
|
||||
// bornage au praticien cible
|
||||
$upd = $pdo->prepare("UPDATE rendez_vous SET statut = :s WHERE id = :id AND praticien_id = :pid");
|
||||
$upd->execute([':s'=>$statut, ':id'=>$id, ':pid'=>$praticien_id]);
|
||||
if ($upd->rowCount() === 0) {
|
||||
out(false, $translations['aucune_modification'] ?? 'Aucune modification.');
|
||||
}
|
||||
// =========================
|
||||
// LOG RDV (rdv_actions_log) : update_status (secrétaire)
|
||||
// =========================
|
||||
$beforeJson = json_encode([
|
||||
'statut' => $beforeRow['statut'],
|
||||
'date_rdv' => $beforeRow['date_rdv'],
|
||||
'duree_minutes' => (int)$beforeRow['duree_minutes'],
|
||||
'lieu' => $beforeRow['lieu'],
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
$afterJson = json_encode([
|
||||
'statut' => $statut,
|
||||
'date_rdv' => $beforeRow['date_rdv'], // date inchangée ici
|
||||
'duree_minutes' => (int)$beforeRow['duree_minutes'], // idem
|
||||
'lieu' => $beforeRow['lieu'],
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
$logStmt = $pdo->prepare("
|
||||
INSERT INTO rdv_actions_log
|
||||
(rdv_id, action, by_user_id, by_role, before_json, after_json, created_at)
|
||||
VALUES
|
||||
(?, 'update_status', ?, 'secretaire', ?, ?, NOW())
|
||||
");
|
||||
$logStmt->execute([(int)$id, (int)$compteId, $beforeJson, $afterJson]);
|
||||
|
||||
// Récup info RDV (+ lieu + adresse praticien)
|
||||
$q = $pdo->prepare("
|
||||
SELECT rv.id, rv.client_id, rv.date_rdv, rv.duree_minutes, rv.uuid_ical, rv.lieu,
|
||||
p.email AS email_praticien, p.prenom AS prenom_praticien, p.nom AS nom_praticien,
|
||||
p.adresse AS p_adresse, p.ville AS p_ville, p.code_postal AS p_cp,
|
||||
c.email AS email_client, c.prenom AS prenom_client, c.nom AS nom_client
|
||||
FROM rendez_vous rv
|
||||
JOIN praticien p ON rv.praticien_id = p.id
|
||||
JOIN client c ON rv.client_id = c.id
|
||||
WHERE rv.id = :id AND rv.praticien_id = :pid
|
||||
");
|
||||
$q->execute([':id'=>$id, ':pid'=>$praticien_id]);
|
||||
$rdv = $q->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$rdv) out(false, $translations['erreur_mise_a_jour'] ?? 'Erreur de mise à jour.');
|
||||
// ✅ compte_id pour badges
|
||||
$clientCompteId = get_compte_id($pdo, 'client', (int)$rdv['client_id']);
|
||||
$praticienCompteId = get_compte_id($pdo, 'praticien', (int)$praticien_id);
|
||||
|
||||
|
||||
// Données communes
|
||||
$tzStr='Europe/Paris'; $tz=new DateTimeZone($tzStr);
|
||||
$start=new DateTime($rdv['date_rdv'], $tz);
|
||||
$end=(clone $start)->modify('+'.(int)($rdv['duree_minutes'] ?? 60).' minutes');
|
||||
$when=$start->format('d/m/Y H:i');
|
||||
$when_a=$start->format('d/m/Y') . ' ' . ($translations['a'] ?? 'в') . ' ' . $start->format('H:i');
|
||||
|
||||
$cliFull = trim(($rdv['prenom_client'] ?? '').' '.($rdv['nom_client'] ?? ''));
|
||||
$pratFull = trim(($rdv['prenom_praticien'] ?? '').' '.($rdv['nom_praticien'] ?? ''));
|
||||
|
||||
// ====== FORMAT (online/offline + adresse) ======
|
||||
$lieu = strtolower(trim((string)($rdv['lieu'] ?? '')));
|
||||
$addrParts = array_filter([
|
||||
trim((string)($rdv['p_adresse'] ?? '')),
|
||||
trim((string)($rdv['p_cp'] ?? '') . ' ' . (string)($rdv['p_ville'] ?? ''))
|
||||
], fn($x) => $x !== '' && $x !== ' ');
|
||||
$addr = trim(implode(', ', $addrParts));
|
||||
|
||||
$formatLabel = $translations['format_label'] ?? 'Формат';
|
||||
$onlineLabel = $translations['option_online'] ?? 'Онлайн';
|
||||
$cabinetLabel = $translations['option_cabinet'] ?? 'Очно';
|
||||
$addressLabel = $translations['adresse_label'] ?? 'Адрес';
|
||||
|
||||
$formatLine = '';
|
||||
if ($lieu === 'online') {
|
||||
$formatLine = "\n\n{$formatLabel}: {$onlineLabel}";
|
||||
} elseif ($lieu === 'offline') {
|
||||
$formatLine = "\n\n{$formatLabel}: {$cabinetLabel}" . ($addr ? "\n{$addressLabel}: {$addr}" : '');
|
||||
}
|
||||
|
||||
// LOCATION pour ICS
|
||||
if ($lieu === 'online') {
|
||||
$icsLocation = $onlineLabel;
|
||||
} elseif ($lieu === 'offline') {
|
||||
$icsLocation = $addr !== '' ? $addr : $cabinetLabel;
|
||||
} else {
|
||||
$icsLocation = '';
|
||||
}
|
||||
|
||||
$cta_client_url='https://uslugi.fr/public/client/mes_rendez_vous.php';
|
||||
$cta_prat_url ='https://uslugi.fr/public/praticien/agenda.php';
|
||||
$cta_label =$translations['consultez_espace_pour_details'] ?? 'Voir les détails';
|
||||
$statutLabelMap=[
|
||||
'confirmé'=>$translations['statut_confirme'] ?? 'Confirmé',
|
||||
'annulé' =>$translations['statut_annule'] ?? 'Annulé',
|
||||
'prévu' =>$translations['statut_prevu'] ?? 'Prévu',
|
||||
'terminé' =>$translations['statut_termine'] ?? 'Terminé',
|
||||
];
|
||||
$statutLibelle=$statutLabelMap[$statut] ?? $statut;
|
||||
|
||||
$warnings=[];
|
||||
|
||||
// ===== HOOK SUIVI CLIENT — création/MAJ dossier + entrée auto (sur confirmation) =====
|
||||
if ($statut === 'confirmé') {
|
||||
// =========================
|
||||
// PUSH RDV — confirmé (par secrétaire)
|
||||
// =========================
|
||||
try {
|
||||
// 🔔 Praticien : confirmé par secrétaire
|
||||
push_rdv_to_praticien(
|
||||
$pdo,
|
||||
(int)$praticien_id,
|
||||
$translations['push_rdv_confirmed_praticien_title'] ?? 'Запись подтверждена',
|
||||
$translations['push_rdv_confirmed_praticien_body'] ?? 'Секретарь подтвердил(а) запись.',
|
||||
'/public/praticien/agenda.php',
|
||||
[
|
||||
'rdv_id' => (int)$rdv['id'],
|
||||
'action' => 'confirm',
|
||||
'from_role' => 'secretaire',
|
||||
'secretaire_id' => (int)$compteId,
|
||||
'client_id' => (int)$rdv['client_id'],
|
||||
'praticien_id' => (int)$praticien_id,
|
||||
'date_rdv' => $rdv['date_rdv'],
|
||||
]
|
||||
);
|
||||
|
||||
// ✅ badges
|
||||
badge_rdv_inc($pdo, $clientCompteId, 1);
|
||||
badge_rdv_inc($pdo, $praticienCompteId, 1);
|
||||
|
||||
push_rdv_to_client(
|
||||
$pdo,
|
||||
(int)$rdv['client_id'],
|
||||
$translations['push_rdv_confirmed_title'] ?? 'Запись подтверждена',
|
||||
$translations['push_rdv_confirmed_body'] ?? 'Ваша запись подтверждена администратором.',
|
||||
'/public/client/mes_rendez_vous.php',
|
||||
[
|
||||
'rdv_id' => (int)$rdv['id'],
|
||||
'action' => 'confirm',
|
||||
'from_role' => 'secretaire',
|
||||
'praticien_id' => (int)$praticien_id,
|
||||
'date_rdv' => $rdv['date_rdv'],
|
||||
]
|
||||
);
|
||||
|
||||
} catch (Throwable $e) {
|
||||
error_log('push rdv confirm (secretaire) failed: '.$e->getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
|
||||
// Upsert dossier praticien↔client
|
||||
$upsert = $pdo->prepare("
|
||||
INSERT INTO client_dossiers (praticien_id, client_id, first_rdv_id)
|
||||
VALUES (:p, :c, :r)
|
||||
ON DUPLICATE KEY UPDATE updated_at = CURRENT_TIMESTAMP
|
||||
");
|
||||
$upsert->execute([
|
||||
':p' => (int)$praticien_id,
|
||||
':c' => (int)$rdv['client_id'],
|
||||
':r' => (int)$rdv['id']
|
||||
]);
|
||||
|
||||
// Récup id dossier (si existed déjà, lastInsertId = 0)
|
||||
$dossierId = (int)$pdo->lastInsertId();
|
||||
if ($dossierId === 0) {
|
||||
$getId = $pdo->prepare("SELECT id FROM client_dossiers WHERE praticien_id=:p AND client_id=:c LIMIT 1");
|
||||
$getId->execute([':p'=>(int)$praticien_id, ':c'=>(int)$rdv['client_id']]);
|
||||
$dossierId = (int)$getId->fetchColumn();
|
||||
}
|
||||
|
||||
// Éviter doublon côté suivi
|
||||
$exists = $pdo->prepare("SELECT 1 FROM client_suivi WHERE dossier_id=:d AND rdv_id=:r LIMIT 1");
|
||||
$exists->execute([':d'=>$dossierId, ':r'=>(int)$rdv['id']]);
|
||||
if (!$exists->fetchColumn()) {
|
||||
$type = ($lieu === 'online') ? 'online' : (($lieu === 'offline') ? 'cabinet' : 'autre');
|
||||
$ins = $pdo->prepare("
|
||||
INSERT INTO client_suivi (dossier_id, rdv_id, date_rdv, type_rdv, titre, notes, prive, duree_minutes)
|
||||
VALUES (:d, :r, :dt, :ty, :ti, :no, 1, :du)
|
||||
");
|
||||
$ins->execute([
|
||||
':d' => $dossierId,
|
||||
':r' => (int)$rdv['id'],
|
||||
':dt' => $start->format('Y-m-d H:i:s'),
|
||||
':ty' => $type,
|
||||
':ti' => $translations['suivi_rdv_confirme_titre'] ?? 'RDV confirmé',
|
||||
':no' => ($translations['suivi_rdv_confirme_note'] ?? 'Créé automatiquement suite à la confirmation du rendez-vous.'),
|
||||
':du' => (int)($rdv['duree_minutes'] ?? 60),
|
||||
]);
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
} catch (Throwable $ex) {
|
||||
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||
error_log('Suivi auto (confirmé/cabinet) failed: '.$ex->getMessage());
|
||||
// on continue quand même (mails, etc.)
|
||||
}
|
||||
|
||||
// E-mails confirmation
|
||||
$d_c = [
|
||||
'hello_line'=>sprintf($translations['mail_valid_bonjour_nom'] ?? 'Bonjour %s',$cliFull),
|
||||
'intro'=>($translations['votre_rdv_confirme'] ?? 'Votre rendez-vous est confirmé').' '.
|
||||
($translations['email_avec'] ?? 'avec').' '.$pratFull.'.',
|
||||
'who_label'=>$translations['praticien_label'] ?? 'Praticien',
|
||||
'who_value'=>$pratFull,
|
||||
'date_label'=>$translations['date_label'] ?? 'Date',
|
||||
'when'=>$when_a,
|
||||
'statut_label'=>$translations['statut_label'] ?? 'Statut',
|
||||
'statut_value'=>$statutLibelle,
|
||||
'cta_url'=>$cta_client_url,
|
||||
'cta_label'=>$cta_label,
|
||||
'footer'=>$translations['merci_simple'] ?? 'Merci.',
|
||||
];
|
||||
if ($formatLine !== '') $d_c['intro'] .= $formatLine;
|
||||
|
||||
$icsC = !empty($rdv['uuid_ical'])
|
||||
? ics_build($rdv['uuid_ical'],$start,$end,$tzStr,
|
||||
($translations['rdv_avec'] ?? 'Rendez-vous avec').' '.$pratFull,
|
||||
'', $icsLocation,
|
||||
[['cn'=>$pratFull,'email'=>$rdv['email_praticien']],['cn'=>$cliFull,'email'=>$rdv['email_client']]],
|
||||
'REQUEST','CONFIRMED')
|
||||
: null;
|
||||
[$okC,$errC] = smtp_send(
|
||||
$rdv['email_client'],
|
||||
$translations['email_confirmation_rdv_sujet'] ?? 'Confirmation de rendez-vous',
|
||||
email_text_status($d_c,$translations),
|
||||
email_html_status($d_c,$translations),
|
||||
$icsC?('rdv_'.$rdv['uuid_ical'].'_client.ics'):null,
|
||||
$icsC,
|
||||
$icsC?'REQUEST':null
|
||||
);
|
||||
if (!$okC) $warnings[]='mail_client';
|
||||
|
||||
$d_p = [
|
||||
'hello_line'=>($translations['email_bonjour_dr'] ?? 'Bonjour').' '.$pratFull,
|
||||
'intro'=>$translations['nouveau_rdv_pris_avec'] ?? 'Un nouveau rendez-vous a été confirmé.',
|
||||
'who_label'=>$translations['client_label'] ?? 'Client',
|
||||
'who_value'=>$cliFull,
|
||||
'date_label'=>$translations['date_label'] ?? 'Date',
|
||||
'when'=>$when_a,
|
||||
'statut_label'=>$translations['statut_label'] ?? 'Statut',
|
||||
'statut_value'=>$statutLibelle,
|
||||
'cta_url'=>$cta_prat_url,
|
||||
'cta_label'=>$cta_label,
|
||||
'footer'=>$translations['merci_simple'] ?? 'Merci.',
|
||||
];
|
||||
if ($formatLine !== '') $d_p['intro'] .= $formatLine;
|
||||
|
||||
$icsP = !empty($rdv['uuid_ical'])
|
||||
? ics_build($rdv['uuid_ical'],$start,$end,$tzStr,
|
||||
($translations['rdv_avec'] ?? 'Rendez-vous avec').' '.$cliFull,
|
||||
'', $icsLocation,
|
||||
[['cn'=>$cliFull,'email'=>$rdv['email_client']],['cn'=>$pratFull,'email'=>$rdv['email_praticien']]],
|
||||
'REQUEST','CONFIRMED')
|
||||
: null;
|
||||
[$okP,$errP] = smtp_send(
|
||||
$rdv['email_praticien'],
|
||||
($translations['rdv_confirme_pour'] ?? 'RDV confirmé pour').' '.$cliFull,
|
||||
email_text_status($d_p,$translations),
|
||||
email_html_status($d_p,$translations),
|
||||
$icsP?('rdv_'.$rdv['uuid_ical'].'_praticien.ics'):null,
|
||||
$icsP,
|
||||
$icsP?'REQUEST':null
|
||||
);
|
||||
if (!$okP) $warnings[]='mail_praticien';
|
||||
}
|
||||
|
||||
if ($statut === 'annulé') {
|
||||
// =========================
|
||||
// PUSH RDV — annulé (par secrétaire)
|
||||
// =========================
|
||||
try {
|
||||
// 🔔 Praticien : annulé par secrétaire
|
||||
push_rdv_to_praticien(
|
||||
$pdo,
|
||||
(int)$praticien_id,
|
||||
$translations['push_rdv_cancel_praticien_title'] ?? 'Запись отменена',
|
||||
$translations['push_rdv_cancel_praticien_body'] ?? 'Секретарь отменил(а) запись.',
|
||||
'/public/praticien/rendez_vous.php',
|
||||
[
|
||||
'rdv_id' => (int)$rdv['id'],
|
||||
'action' => 'cancel',
|
||||
'from_role' => 'secretaire',
|
||||
'secretaire_id' => (int)$compteId,
|
||||
'client_id' => (int)$rdv['client_id'],
|
||||
'praticien_id' => (int)$praticien_id,
|
||||
'date_rdv' => $rdv['date_rdv'],
|
||||
]
|
||||
);
|
||||
|
||||
// ✅ badges
|
||||
badge_rdv_inc($pdo, $clientCompteId, 1);
|
||||
badge_rdv_inc($pdo, $praticienCompteId, 1);
|
||||
|
||||
push_rdv_to_client(
|
||||
$pdo,
|
||||
(int)$rdv['client_id'],
|
||||
$translations['push_rdv_cancel_secretaire_title'] ?? 'Запись отменена',
|
||||
$translations['push_rdv_cancel_secretaire_body'] ?? 'Администратор отменил запись.',
|
||||
'/public/client/mes_rendez_vous.php',
|
||||
[
|
||||
'rdv_id' => (int)$rdv['id'],
|
||||
'action' => 'cancel',
|
||||
'from_role' => 'secretaire',
|
||||
'praticien_id' => (int)$praticien_id,
|
||||
'date_rdv' => $rdv['date_rdv'],
|
||||
]
|
||||
);
|
||||
|
||||
} catch (Throwable $e) {
|
||||
error_log('push rdv cancel (secretaire) failed: '.$e->getMessage());
|
||||
}
|
||||
|
||||
$summary = ($translations['rdv_avec'] ?? 'Rendez-vous avec').' '.$pratFull;
|
||||
$attBoth = [
|
||||
['cn'=>$cliFull,'email'=>$rdv['email_client']],
|
||||
['cn'=>$pratFull,'email'=>$rdv['email_praticien']]
|
||||
];
|
||||
$ics = !empty($rdv['uuid_ical'])
|
||||
? ics_build($rdv['uuid_ical'],$start,$end,$tzStr,$summary,'',$icsLocation, $attBoth,'CANCEL','CANCELLED')
|
||||
: null;
|
||||
|
||||
$d_c = [
|
||||
'hello_line'=>sprintf($translations['mail_valid_bonjour_nom'] ?? 'Bonjour %s',$cliFull),
|
||||
'intro'=>($translations['email_votre_rdv_du'] ?? 'Votre rendez-vous du').' '.$when_a.' '.
|
||||
($translations['email_avec'] ?? 'avec').' '.$pratFull.' '.
|
||||
($translations['email_a_ete_annule'] ?? 'a été annulé.'),
|
||||
'who_label'=>$translations['praticien_label'] ?? 'Praticien',
|
||||
'who_value'=>$pratFull,
|
||||
'date_label'=>$translations['date_label'] ?? 'Date',
|
||||
'when'=>$when_a,
|
||||
'statut_label'=>$translations['statut_label'] ?? 'Statut',
|
||||
'statut_value'=>$statutLibelle,
|
||||
'cta_url'=>'https://uslugi.fr/public/client/mes_rendez_vous.php',
|
||||
'cta_label'=>$translations['consultez_espace_pour_details'] ?? 'Voir les détails',
|
||||
'footer'=>$translations['merci_simple'] ?? 'Merci.',
|
||||
];
|
||||
[$okC,$errC] = smtp_send(
|
||||
$rdv['email_client'],
|
||||
$translations['email_annulation_client_sujet'] ?? 'Confirmation d’annulation de votre rendez-vous',
|
||||
email_text_status($d_c,$translations),
|
||||
email_html_status($d_c,$translations),
|
||||
$ics?('rdv_cancel_'.$rdv['uuid_ical'].'_client.ics'):null,
|
||||
$ics,
|
||||
$ics?'CANCEL':null
|
||||
);
|
||||
if (!$okC) $warnings[]='mail_client';
|
||||
|
||||
$d_p = [
|
||||
'hello_line'=>($translations['email_bonjour_dr'] ?? 'Bonjour').' '.$pratFull,
|
||||
'intro'=>($translations['email_rdv_du'] ?? 'Le rendez-vous du').' '.$when_a.' '.
|
||||
($translations['email_avec'] ?? 'avec').' '.$cliFull.' '.
|
||||
($translations['email_annule_par_praticien'] ?? 'a été annulé.'),
|
||||
'who_label'=>$translations['client_label'] ?? 'Client',
|
||||
'who_value'=>$cliFull,
|
||||
'date_label'=>$translations['date_label'] ?? 'Date',
|
||||
'when'=>$when_a,
|
||||
'statut_label'=>$translations['statut_label'] ?? 'Statut',
|
||||
'statut_value'=>$statutLibelle,
|
||||
'cta_url'=>'https://uslugi.fr/public/praticien/agenda.php',
|
||||
'cta_label'=>$translations['consultez_espace_pour_details'] ?? 'Voir les détails',
|
||||
'footer'=>$translations['merci_simple'] ?? 'Merci.',
|
||||
];
|
||||
[$okP,$errP] = smtp_send(
|
||||
$rdv['email_praticien'],
|
||||
$translations['email_annulation_sujet'] ?? 'Rendez-vous annulé',
|
||||
email_text_status($d_p,$translations),
|
||||
email_html_status($d_p,$translations),
|
||||
$ics?('rdv_cancel_'.$rdv['uuid_ical'].'_praticien.ics'):null,
|
||||
$ics,
|
||||
$ics?'CANCEL':null
|
||||
);
|
||||
if (!$okP) $warnings[]='mail_praticien';
|
||||
}
|
||||
|
||||
if ($statut === 'terminé') {
|
||||
// =========================
|
||||
// PUSH RDV — terminé (par secrétaire)
|
||||
// =========================
|
||||
try {
|
||||
// 🔔 Praticien : marqué terminé par secrétaire
|
||||
push_rdv_to_praticien(
|
||||
$pdo,
|
||||
(int)$praticien_id,
|
||||
$translations['push_rdv_done_praticien_title'] ?? 'Приём завершён',
|
||||
$translations['push_rdv_done_praticien_body'] ?? 'Секретарь отметил(а) приём как завершённый.',
|
||||
'/public/praticien/agenda.php',
|
||||
[
|
||||
'rdv_id' => (int)$rdv['id'],
|
||||
'action' => 'done',
|
||||
'from_role' => 'secretaire',
|
||||
'secretaire_id' => (int)$compteId,
|
||||
'client_id' => (int)$rdv['client_id'],
|
||||
'praticien_id' => (int)$praticien_id,
|
||||
'date_rdv' => $rdv['date_rdv'],
|
||||
]
|
||||
);
|
||||
|
||||
// ✅ badges
|
||||
badge_rdv_inc($pdo, $clientCompteId, 1);
|
||||
badge_rdv_inc($pdo, $praticienCompteId, 1);
|
||||
|
||||
push_rdv_to_client(
|
||||
$pdo,
|
||||
(int)$rdv['client_id'],
|
||||
$translations['push_rdv_done_title'] ?? 'Приём завершён',
|
||||
$translations['push_rdv_done_body'] ?? 'Ваш визит завершён.',
|
||||
'/public/client/mes_rendez_vous.php',
|
||||
[
|
||||
'rdv_id' => (int)$rdv['id'],
|
||||
'action' => 'done',
|
||||
'from_role' => 'secretaire',
|
||||
'praticien_id' => (int)$praticien_id,
|
||||
]
|
||||
);
|
||||
|
||||
} catch (Throwable $e) {
|
||||
error_log('push rdv done (secretaire) failed: '.$e->getMessage());
|
||||
}
|
||||
|
||||
$d_c = [
|
||||
'hello_line'=>sprintf($translations['mail_valid_bonjour_nom'] ?? 'Bonjour %s',$cliFull),
|
||||
'intro'=>$translations['email_rdv_termine_message_client'] ?? 'Votre rendez-vous est marqué comme terminé. Merci pour votre confiance.',
|
||||
'who_label'=>$translations['praticien_label'] ?? 'Praticien',
|
||||
'who_value'=>$pratFull,
|
||||
'date_label'=>$translations['date_label'] ?? 'Date',
|
||||
'when'=>$when_a,
|
||||
'statut_label'=>$translations['statut_label'] ?? 'Statut',
|
||||
'statut_value'=>$statutLibelle,
|
||||
'cta_url' => 'https://uslugi.fr/public/client/mes_rendez_vous.php',
|
||||
'cta_label'=>$translations['avis_suite_consultation'] ?? 'Voir mes rendez-vous',
|
||||
'footer'=>$translations['merci_simple'] ?? 'Merci.',
|
||||
];
|
||||
[$okC,$errC] = smtp_send(
|
||||
$rdv['email_client'],
|
||||
$translations['email_rdv_termine_client_sujet'] ?? 'Merci de votre visite',
|
||||
email_text_status($d_c,$translations),
|
||||
email_html_status($d_c,$translations)
|
||||
);
|
||||
if (!$okC) $warnings[]='mail_client';
|
||||
|
||||
$d_p = [
|
||||
'hello_line'=>($translations['email_bonjour_dr'] ?? 'Bonjour').' '.$pratFull,
|
||||
'intro'=>$translations['email_rdv_termine_message_praticien'] ?? 'Vous avez marqué ce rendez-vous comme terminé.',
|
||||
'who_label'=>$translations['client_label'] ?? 'Client',
|
||||
'who_value'=>$cliFull,
|
||||
'date_label'=>$translations['date_label'] ?? 'Date',
|
||||
'when'=>$when_a,
|
||||
'statut_label'=>$translations['statut_label'] ?? 'Statut',
|
||||
'statut_value'=>$statutLibelle,
|
||||
'cta_url'=>'https://uslugi.fr/public/praticien/agenda.php',
|
||||
'cta_label'=>$translations['consultez_espace_pour_details'] ?? 'Voir les détails',
|
||||
'footer'=>$translations['merci_simple'] ?? 'Merci.',
|
||||
];
|
||||
[$okP,$errP] = smtp_send(
|
||||
$rdv['email_praticien'],
|
||||
$translations['email_rdv_termine_praticien_sujet'] ?? 'Rendez-vous marqué terminé',
|
||||
email_text_status($d_p,$translations),
|
||||
email_html_status($d_p,$translations)
|
||||
);
|
||||
if (!$okP) $warnings[]='mail_praticien';
|
||||
}
|
||||
|
||||
$msg = $translations['statut_mis_a_jour'] ?? 'Statut mis à jour.';
|
||||
if ($warnings) $msg .= ' (mail: '.implode(',',$warnings).')';
|
||||
out(true, $msg, ['status'=>$statut]);
|
||||
|
||||
} catch (Throwable $e) {
|
||||
error_log('changer_statut (cabinet) fatal: '.$e->getMessage());
|
||||
out(false, $translations['erreur_mise_a_jour'] ?? 'Erreur de mise à jour.');
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../includes/init_secretaire.php';
|
||||
|
||||
|
||||
|
||||
if (!function_exists('h')) { function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); } }
|
||||
|
||||
$sec_uid = (int)($_SESSION['compte_id'] ?? 0);
|
||||
if ($sec_uid<=0) { header('Location:/index.php'); exit; }
|
||||
|
||||
// Liste praticiens
|
||||
$q = $pdo->prepare("
|
||||
SELECT sp.id as spid, sp.praticien_id, sp.can_manage_rdv, sp.can_edit_slots, sp.is_active,
|
||||
p.nom, p.prenom, p.specialite, p.ville
|
||||
FROM secretaire_praticien sp
|
||||
JOIN praticien p ON p.id = sp.praticien_id
|
||||
WHERE sp.secretaire_user_id = :u AND sp.is_active=1
|
||||
ORDER BY p.nom, p.prenom
|
||||
");
|
||||
$q->execute(['u'=>$sec_uid]);
|
||||
$rows = $q->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Compteurs simples Today/Tomorrow par praticien
|
||||
$today = (new DateTime('today'))->format('Y-m-d 00:00:00');
|
||||
$tomorrow = (new DateTime('tomorrow'))->format('Y-m-d 00:00:00');
|
||||
|
||||
$counts = [];
|
||||
if ($rows) {
|
||||
$ids = array_column($rows,'praticien_id');
|
||||
$in = implode(',', array_fill(0, count($ids), '?'));
|
||||
$s = $pdo->prepare("
|
||||
SELECT praticien_id,
|
||||
SUM(date_rdv >= ? AND date_rdv < ?) AS today_count,
|
||||
SUM(date_rdv >= ? AND date_rdv < DATE_ADD(?, INTERVAL 1 DAY)) AS tomorrow_count
|
||||
FROM rendez_vous
|
||||
WHERE praticien_id IN ($in) AND statut IN ('prévu','confirmé')
|
||||
GROUP BY praticien_id
|
||||
");
|
||||
$params = [$today,(new DateTime('tomorrow'))->format('Y-m-d 00:00:00'), (new DateTime('tomorrow'))->format('Y-m-d 00:00:00'), (new DateTime('tomorrow'))->format('Y-m-d 00:00:00')];
|
||||
$s->execute(array_merge($params, $ids));
|
||||
foreach($s->fetchAll(PDO::FETCH_ASSOC) as $r){ $counts[(int)$r['praticien_id']] = $r; }
|
||||
}
|
||||
require_once __DIR__ . '/../includes/header_cabinet.php';
|
||||
?>
|
||||
<div class="main1">
|
||||
<div class="shadow p-4 rounded bg-light">
|
||||
<h1 class="h4 mb-3"><?= h($translations['sec_dashboard_titre'] ?? 'Secrétariat — Tableau de bord') ?></h1>
|
||||
|
||||
<?php if(!$rows): ?>
|
||||
<div class="alert alert-info"><?= h($translations['sec_aucun_praticien'] ?? "Aucun praticien lié pour l’instant.") ?></div>
|
||||
<?php else: ?>
|
||||
<div class="row g-4">
|
||||
<?php foreach($rows as $r):
|
||||
$pid = (int)$r['praticien_id'];
|
||||
$c = $counts[$pid] ?? ['today_count'=>0,'tomorrow_count'=>0];
|
||||
?>
|
||||
<div class="col-12 col-md-6 col-lg-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||
<div>
|
||||
<div class="fw-semibold"><?= h($r['prenom'].' '.$r['nom']) ?></div>
|
||||
<div class="text-muted small"><?= h(trim(($r['specialite']??'').' · '.($r['ville']??''), ' · ')) ?></div>
|
||||
</div>
|
||||
<span class="badge bg-secondary"><?= h($translations['sec_actif'] ?? 'Actif') ?></span>
|
||||
</div>
|
||||
<div class="d-flex gap-3 mb-3">
|
||||
<div><span class="fw-bold"><?= (int)$c['today_count'] ?></span> <span class="text-muted small"><?= h($translations['sec_aujourdhui'] ?? 'aujourd’hui') ?></span></div>
|
||||
<div><span class="fw-bold"><?= (int)$c['tomorrow_count'] ?></span> <span class="text-muted small"><?= h($translations['sec_demain'] ?? 'demain') ?></span></div>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<a class="btn btn-primary btn-sm" href="agenda.php?praticien_id=<?= $pid ?>"><?= h($translations['choisir'] ?? 'Choisir') ?></a>
|
||||
<?php if ($r['can_manage_rdv']): ?>
|
||||
<!--<a class="btn btn-outline-secondary btn-sm" href="agenda.php?praticien_id=<?= $pid ?>"><?= h($translations['choisir'] ?? 'Choisir') ?></a>
|
||||
<a class="btn btn-outline-secondary btn-sm" href="rendez_vous_du_jour.php?praticien_id=<?= $pid ?>"><?= h($translations['sec_btn_rdv'] ?? 'Rendez-vous') ?></a>
|
||||
<a class="btn btn-outline-secondary btn-sm" href="rendez_vous.php?praticien_id=<?= $pid ?>"><?= h($translations['sec_btn_rdv_jour'] ?? 'Les rendez-vous') ?></a>
|
||||
<a class="btn btn-outline-secondary btn-sm" href="reserver_rdv.php?praticien_id=<?= $pid ?>"><?= h($translations['sec_btn_prendre'] ?? 'Prendre un RDV') ?></a>-->
|
||||
<?php endif; ?>
|
||||
<?php if ($r['can_edit_slots']): ?>
|
||||
<!--<a class="btn btn-outline-secondary btn-sm" href="disponibilites.php?praticien_id=<?= $pid ?>"><?= h($translations['sec_btn_dispos'] ?? 'Disponibilités') ?></a>-->
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p><?= $translations['copyr'] ?? '© '.date('Y') ?></p>
|
||||
</div>
|
||||
</footer>
|
||||
@@ -0,0 +1,269 @@
|
||||
<?php
|
||||
// /public/cabinet/disponibilites.php (VERSION SECRÉTAIRE)
|
||||
require_once __DIR__ . '/../includes/init_secretaire.php';
|
||||
|
||||
|
||||
// --- Helpers
|
||||
if (!function_exists('h')) { function h($s){ 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'); }
|
||||
|
||||
// --- 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) { http_response_code(403); exit('Forbidden'); }
|
||||
|
||||
// ---- 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']);
|
||||
|
||||
// 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) {
|
||||
$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');
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 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);
|
||||
|
||||
|
||||
// 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>
|
||||
HTML;
|
||||
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"
|
||||
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>
|
||||
|
||||
<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="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="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>
|
||||
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
<?= h($translations['btn_ajouter'] ?? 'Ajouter') ?>
|
||||
</button>
|
||||
</form>
|
||||
</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) : '' ?>';
|
||||
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
if (erreurMsg) {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: '<?= addslashes($translations["erreur_titre"] ?? "Erreur") ?>',
|
||||
text: erreurMsg,
|
||||
confirmButtonText: 'OK'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// --- Confirmation de suppression avec SweetAlert2 ---
|
||||
document.addEventListener('click', async function(e){
|
||||
const btn = e.target.closest('.js-del');
|
||||
if (!btn) return;
|
||||
const id = btn.dataset.id;
|
||||
|
||||
const result = await Swal.fire({
|
||||
icon: 'warning',
|
||||
title: '<?= addslashes($translations["confirm_title"] ?? "Confirmation") ?>',
|
||||
text: '<?= addslashes($translations["confirm_supprimer_creneau"] ?? "Supprimer ce créneau ?") ?>',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: '<?= addslashes($translations["btn_da_udalit"] ?? "Oui") ?>',
|
||||
cancelButtonText: '<?= addslashes($translations["btn_annuler"] ?? "Annuler") ?>',
|
||||
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);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!--*********Adaptation des tables pour un affichage mobile***************-->
|
||||
<script defer>
|
||||
(function(){
|
||||
function initMobileCards(tbl){
|
||||
if (!tbl) return;
|
||||
const heads = Array.from(tbl.querySelectorAll('thead th')).map(th => th.textContent.trim());
|
||||
tbl.querySelectorAll('tbody tr').forEach(tr => {
|
||||
Array.from(tr.children).forEach((td, i) => {
|
||||
if (!td.hasAttribute('data-label')) td.setAttribute('data-label', heads[i] || '');
|
||||
});
|
||||
});
|
||||
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')); });
|
||||
})();
|
||||
</script>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p><?= $translations['copyr'] ?? '© '.date('Y') ?></p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,252 @@
|
||||
<?php
|
||||
// /public/cabinet/disponibilites_date.php (VERSION SECRÉTAIRE)
|
||||
require_once __DIR__ . '/../includes/init_secretaire.php';
|
||||
|
||||
|
||||
if (!function_exists('h')) {
|
||||
function h($s){ 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;
|
||||
|
||||
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
|
||||
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) { http_response_code(403); exit('Forbidden'); }
|
||||
|
||||
// --- 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'] ?? ''));
|
||||
|
||||
$erreur = null;
|
||||
|
||||
// ---- 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
|
||||
|
||||
$tz = new DateTimeZone('Europe/Paris');
|
||||
$today = (new DateTimeImmutable('now', $tz))->format('Y-m-d');
|
||||
|
||||
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)) {
|
||||
$erreur = $translations['erreur_format_heure'] ?? "Format d’heure invalide.";
|
||||
} elseif ($date < $today) {
|
||||
$erreur = $translations['erreur_date_passee'] ?? "Date dans le passé.";
|
||||
} elseif ($debut >= $fin) {
|
||||
$erreur = $translations['erreur_debut_avant_fin'] ?? "Heure de début après l’heure de fin.";
|
||||
}
|
||||
|
||||
if (!$erreur) {
|
||||
try {
|
||||
$stmt = $pdo->prepare("
|
||||
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',
|
||||
]);
|
||||
|
||||
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
|
||||
FROM disponibilite_date
|
||||
WHERE praticien_id = ?
|
||||
ORDER BY date_dispo ASC, heure_debut ASC
|
||||
");
|
||||
$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>
|
||||
HTML;
|
||||
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>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm p-4">
|
||||
<h4 class="mb-3 text-center"><?= h($translations['card_ajouter_creneau_date'] ?? 'Ajouter une disponibilité ponctuelle') ?></h4>
|
||||
|
||||
<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="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="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>
|
||||
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
<?= h($translations['btn_ajouter'] ?? 'Ajouter') ?>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function(){
|
||||
const okParam = '<?= isset($_GET['ok']) ? addslashes((string)$_GET['ok']) : '' ?>';
|
||||
const erreurMsg = '<?= $erreur ? addslashes((string)$erreur) : '' ?>';
|
||||
|
||||
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 });
|
||||
}
|
||||
|
||||
if (erreurMsg) {
|
||||
Swal.fire({ icon:'error', title:'<?= addslashes($translations["erreur_titre"] ?? "Erreur") ?>', 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;
|
||||
|
||||
const result = await Swal.fire({
|
||||
icon: 'warning',
|
||||
title: '<?= addslashes($translations["confirm_title"] ?? "Confirmation") ?>',
|
||||
text: '<?= addslashes($translations["confirm_supprimer_creneau"] ?? "Supprimer ce créneau ?") ?>',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: '<?= addslashes($translations["btn_da_udalit"] ?? "Oui") ?>',
|
||||
cancelButtonText: '<?= addslashes($translations["btn_annuler"] ?? "Annuler") ?>',
|
||||
reverseButtons: true
|
||||
});
|
||||
|
||||
if (result.isConfirmed) {
|
||||
const pid = '<?= (int)$praticien_id ?>';
|
||||
window.location.href = '?praticien_id=' + encodeURIComponent(pid) + '&supprimer=' + encodeURIComponent(id);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<script defer>
|
||||
(function(){
|
||||
function initMobileCards(tbl){
|
||||
if (!tbl) return;
|
||||
const heads = Array.from(tbl.querySelectorAll('thead th')).map(th => th.textContent.trim());
|
||||
tbl.querySelectorAll('tbody tr').forEach(tr => {
|
||||
Array.from(tr.children).forEach((td, i) => {
|
||||
if (!td.hasAttribute('data-label')) td.setAttribute('data-label', heads[i] || '');
|
||||
});
|
||||
});
|
||||
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')); });
|
||||
})();
|
||||
</script>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p><?= $translations['copyr'] ?? '© '.date('Y') ?></p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
// /public/cabinet/events.php
|
||||
require_once __DIR__ . '/../includes/init_secretaire.php';
|
||||
|
||||
|
||||
// --- Inputs ---
|
||||
$compteId = (int)($_SESSION['compte_id'] ?? 0);
|
||||
$praticien_id = (int)($_POST['praticien_id'] ?? $_GET['praticien_id'] ?? 0);
|
||||
|
||||
// FullCalendar envoie start/end en ISO (UTC). On les rend optionnels.
|
||||
$startIso = isset($_POST['start']) ? (string)$_POST['start'] : (isset($_GET['start']) ? (string)$_GET['start'] : null);
|
||||
$endIso = isset($_POST['end']) ? (string)$_POST['end'] : (isset($_GET['end']) ? (string)$_GET['end'] : null);
|
||||
|
||||
if ($compteId <= 0 || $praticien_id <= 0) { echo json_encode([]); exit; }
|
||||
|
||||
// --- Sécurité: vérifier appartenance secrétaire → praticien ---
|
||||
$chk = $pdo->prepare("
|
||||
SELECT 1
|
||||
FROM secretaire_praticien
|
||||
WHERE secretaire_user_id = :u
|
||||
AND praticien_id = :p
|
||||
AND is_active = 1
|
||||
LIMIT 1
|
||||
");
|
||||
$chk->execute(['u'=>$compteId,'p'=>$praticien_id]);
|
||||
if (!$chk->fetchColumn()) { echo json_encode([]); exit; }
|
||||
|
||||
// --- Requête RDV (avec plage si fournie) ---
|
||||
$params = [':p' => $praticien_id];
|
||||
$whereDates = '';
|
||||
|
||||
if ($startIso && $endIso) {
|
||||
// On normalise en 'Y-m-d H:i:s'
|
||||
$startSql = date('Y-m-d H:i:s', strtotime($startIso));
|
||||
$endSql = date('Y-m-d H:i:s', strtotime($endIso));
|
||||
$whereDates = " AND rv.date_rdv BETWEEN :d1 AND :d2 ";
|
||||
$params[':d1'] = $startSql;
|
||||
$params[':d2'] = $endSql;
|
||||
}
|
||||
|
||||
$sql = "
|
||||
SELECT rv.id, rv.date_rdv, rv.duree_minutes, rv.statut, rv.lieu,
|
||||
c.nom, c.prenom
|
||||
FROM rendez_vous rv
|
||||
JOIN client c ON rv.client_id = c.id
|
||||
WHERE rv.praticien_id = :p
|
||||
$whereDates
|
||||
ORDER BY rv.date_rdv ASC
|
||||
";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$rdvs = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// --- Mappage couleurs par statut ---
|
||||
$couleurs = [
|
||||
'prévu' => '#f39c12', // orange
|
||||
'confirmé' => '#27ae60', // vert
|
||||
'annulé' => '#c0392b', // rouge
|
||||
'terminé' => '#7f8c8d', // gris
|
||||
];
|
||||
|
||||
// --- Construction des events ---
|
||||
$tz = new DateTimeZone('Europe/Paris');
|
||||
$events = [];
|
||||
|
||||
foreach ($rdvs as $r) {
|
||||
$start = new DateTime($r['date_rdv'], $tz);
|
||||
$end = (clone $start)->modify('+' . (int)($r['duree_minutes'] ?? 60) . ' minutes');
|
||||
$statut = (string)$r['statut'];
|
||||
|
||||
$events[] = [
|
||||
'id' => (int)$r['id'],
|
||||
'title' => trim(($r['prenom'] ?? '') . ' ' . ($r['nom'] ?? '')),
|
||||
'start' => $start->format(DateTime::ATOM), // ISO 8601
|
||||
'end' => $end->format(DateTime::ATOM),
|
||||
'allDay' => false,
|
||||
'color' => $couleurs[$statut] ?? '#999',
|
||||
// Champ non standard → disponible via event.extendedProps.statut côté FC
|
||||
'statut' => ucfirst($statut),
|
||||
];
|
||||
}
|
||||
|
||||
echo json_encode($events, JSON_UNESCAPED_UNICODE);
|
||||
@@ -0,0 +1,320 @@
|
||||
<?php
|
||||
// /public/cabinet/invitations.php
|
||||
require_once __DIR__ . '/../includes/init_praticien.php';
|
||||
|
||||
|
||||
|
||||
if (!function_exists('h')) { function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); } }
|
||||
$praticien_id = (int)($_SESSION['reference_id'] ?? 0);
|
||||
if ($praticien_id<=0) { header('Location:/index.php'); exit; }
|
||||
|
||||
// CSRF
|
||||
if (empty($_SESSION['csrf'])) $_SESSION['csrf']=bin2hex(random_bytes(16));
|
||||
$csrf = $_SESSION['csrf'];
|
||||
|
||||
// Handle POST (invite or toggle)
|
||||
$msg = '';
|
||||
if (($_SERVER['REQUEST_METHOD']??'')==='POST' && hash_equals($csrf, $_POST['csrf'] ?? '')) {
|
||||
|
||||
if (isset($_POST['action']) && $_POST['action']==='invite') {
|
||||
$email = trim((string)($_POST['email'] ?? ''));
|
||||
$can_manage_rdv = !empty($_POST['can_manage_rdv']) ? 1 : 0;
|
||||
$can_edit_slots = !empty($_POST['can_edit_slots']) ? 1 : 0;
|
||||
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
$msg = $translations['inv_email_invalide'] ?? 'Email invalide.';
|
||||
} else {
|
||||
// token & expiration
|
||||
$token = bin2hex(random_bytes(24));
|
||||
$expires = (new DateTime('+10 days'))->format('Y-m-d H:i:s');
|
||||
|
||||
// Insert invitation (role 'secretaire')
|
||||
$ins = $pdo->prepare("INSERT INTO invitations (email, role, praticien_id, token, status, expires_at, invited_by)
|
||||
VALUES (:email,'secretaire',:pid,:tk,'envoyee',:exp,:by)");
|
||||
$ins->execute([
|
||||
'email'=>$email,
|
||||
'pid'=>$praticien_id,
|
||||
'tk'=>$token,
|
||||
'exp'=>$expires,
|
||||
'by'=> (int)($_SESSION['compte_id'] ?? 0),
|
||||
]);
|
||||
|
||||
// Stocker droits sélectionnés côté "mémo" (façon simple : dans token via table dédiée ?)
|
||||
// Ici on ajoute un petit "mémo" côté invitations.meta_json si tu souhaites :
|
||||
// Si tu veux, ajoute une colonne meta_json JSON dans invitations. Sinon, on appliquera des droits par défaut à l'acceptation, et on pourra basculer ensuite.
|
||||
// Pour rester 100% compatible avec ta structure actuelle, on applique par défaut can_manage_rdv=1, can_edit_slots=0, puis on laisse l'UI permettre de modifier après acceptation.
|
||||
|
||||
// Envoi email (si tu as un mailer)
|
||||
if (function_exists('uslugi_mail')) {
|
||||
$acceptUrl = sprintf(
|
||||
'%s/public/cabinet/accepter_invitation.php?token=%s',
|
||||
rtrim((isset($_SERVER['REQUEST_SCHEME']) ? $_SERVER['REQUEST_SCHEME'].'://' : '').($_SERVER['HTTP_HOST']??''), '/'),
|
||||
$token
|
||||
);
|
||||
|
||||
$subject = $translations['inv_mail_sujet']
|
||||
?? 'Invitation à rejoindre un cabinet sur uslugi.fr';
|
||||
|
||||
$body = '
|
||||
<div style="font-family:Arial,Helvetica,sans-serif;font-size:15px;color:#333;background:#f9f9fb;padding:20px;">
|
||||
<div style="max-width:600px;margin:0 auto;background:#fff;border-radius:12px;overflow:hidden;box-shadow:0 2px 10px rgba(0,0,0,.05);">
|
||||
|
||||
|
||||
|
||||
<!-- Titre -->
|
||||
<div style="background:#38378B;color:#fff;padding:18px 24px;font-size:18px;font-weight:bold;">
|
||||
<img src="https://uslugi.fr/public/uploads/img/logo_mail.png"
|
||||
alt="uslugi.fr" style="max-height:30px;display:block;">
|
||||
</div>
|
||||
|
||||
<!-- Contenu -->
|
||||
<div style="padding:24px;">
|
||||
<p>'.h($translations['inv_mail_bonjour'] ?? 'Bonjour,').'</p>
|
||||
|
||||
<p>'.
|
||||
h($translations['inv_mail_intro'] ??
|
||||
'Vous avez été invité(e) à rejoindre le secrétariat d’un praticien sur uslugi.fr.
|
||||
Grâce à cet accès, vous pourrez gérer les rendez-vous et organiser l’agenda du cabinet.').
|
||||
'</p>
|
||||
|
||||
<p style="margin:24px 0;text-align:center;">
|
||||
<a href="'.h($acceptUrl).'"
|
||||
style="background:#E03C3F;color:#fff;text-decoration:none;
|
||||
padding:12px 22px;border-radius:999px;font-weight:bold;display:inline-block;">
|
||||
'.h($translations['inv_mail_btn'] ?? 'Accepter l’invitation').'
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p>'.
|
||||
h($translations['inv_mail_validite'] ??
|
||||
'Ce lien est valide pendant 10 jours. Passé ce délai, il expirera automatiquement.').
|
||||
'</p>
|
||||
|
||||
<p style="font-size:13px;color:#777;margin-top:24px;">
|
||||
'.h($translations['inv_mail_ignore'] ??
|
||||
'Si vous n’êtes pas concerné(e) par cette invitation, vous pouvez ignorer cet email.').'
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div style="background:#f1f1f1;color:#555;padding:12px 24px;font-size:13px;text-align:center;">
|
||||
'.h($translations['inv_mail_footer'] ?? ('© '.date('Y').' uslugi.fr – Tous droits réservés')).'
|
||||
</div>
|
||||
</div>
|
||||
</div>';
|
||||
|
||||
uslugi_mail($email, $subject, $body);
|
||||
}
|
||||
|
||||
|
||||
|
||||
$msg = $translations['invitation_envoyee'] ?? 'Invitation envoyée.';
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_POST['action']) && $_POST['action']==='toggle_rights') {
|
||||
$spid = (int)($_POST['spid'] ?? 0);
|
||||
$field = ($_POST['field'] ?? '');
|
||||
if ($spid>0 && in_array($field,['can_manage_rdv','can_edit_slots','is_active'],true)) {
|
||||
// secure update scoped to this praticien
|
||||
$upd = $pdo->prepare("UPDATE secretaire_praticien SET $field = 1 - $field WHERE id=:id AND praticien_id=:pid");
|
||||
$upd->execute(['id'=>$spid,'pid'=>$praticien_id]);
|
||||
$msg = $translations['droits_mis_a_jour'] ?? 'Droits mis à jour.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DATA: invitations + secretaire_praticien liés à CE praticien
|
||||
$invitations = $pdo->prepare("SELECT id,email,status,expires_at,created_at FROM invitations WHERE praticien_id=:pid ORDER BY created_at DESC");
|
||||
$invitations->execute(['pid'=>$praticien_id]);
|
||||
$invRows = $invitations->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// secrétaires liées
|
||||
$secs = $pdo->prepare("
|
||||
SELECT sp.id, sp.secretaire_user_id, sp.can_manage_rdv, sp.can_edit_slots, sp.is_active, sp.created_at,
|
||||
c.email
|
||||
FROM secretaire_praticien sp
|
||||
JOIN compte c ON c.id = sp.secretaire_user_id
|
||||
WHERE sp.praticien_id = :pid
|
||||
ORDER BY sp.created_at DESC
|
||||
");
|
||||
$secs->execute(['pid'=>$praticien_id]);
|
||||
$secRows = $secs->fetchAll(PDO::FETCH_ASSOC);
|
||||
require_once __DIR__ . '/../includes/header.php';
|
||||
?>
|
||||
<div class="main1">
|
||||
<div class="container py-5">
|
||||
<div class="shadow p-4 rounded bg-light">
|
||||
<h1 class="h3 mb-3"><?= h($translations['secretariat_titre'] ?? 'Secrétariat') ?></h1>
|
||||
|
||||
<?php if(!empty($msg)): ?>
|
||||
<div class="alert alert-info"><?= h($msg) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<h2 class="h5 mb-3"><?= h($translations['secretariat_inviter_secretaire'] ?? 'Inviter une secrétaire') ?></h2>
|
||||
<form method="post" class="row g-3">
|
||||
<input type="hidden" name="csrf" value="<?= h($csrf) ?>">
|
||||
<input type="hidden" name="action" value="invite">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label"><?= h($translations['inv_email_label'] ?? 'Email de la secrétaire') ?></label>
|
||||
<input type="email" name="email" class="form-control" required placeholder="email@example.com">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label"><?= h($translations['inv_droits_label'] ?? 'Droits initiaux') ?></label>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="can_manage_rdv" id="c1" checked>
|
||||
<label class="form-check-label" for="c1"><?= h($translations['droits_peut_gestion_rdv'] ?? 'Gérer les rendez-vous') ?></label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="can_edit_slots" id="c2" checked>
|
||||
<label class="form-check-label" for="c2"><?= h($translations['droits_peut_modifier_slots'] ?? 'Modifier les disponibilités') ?></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<button class="btn btn-primary"><?= h($translations['btn_envoyer_invitation'] ?? 'Envoyer l’invitation') ?></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<h2 class="h6 mb-3"><?= h($translations['inv_invitations_envoyees'] ?? 'Invitations envoyées') ?></h2>
|
||||
<div class="table-responsive">
|
||||
<table class="table align-middle mobile-cards1" id="table_rdv1">
|
||||
<thead><tr>
|
||||
<th>Email</th><th><?= h($translations['inv_statut'] ?? 'Statut') ?></th>
|
||||
<th><?= h($translations['inv_expires'] ?? 'Expire le') ?></th>
|
||||
<th><?= h($translations['inv_envoyee_le'] ?? 'Envoyée le') ?></th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<?php foreach($invRows as $r): ?>
|
||||
<tr>
|
||||
<td><?= h($r['email']) ?></td>
|
||||
<td><?= h($r['status']) ?></td>
|
||||
<td><?= h($r['expires_at'] ?? '') ?></td>
|
||||
<td><?= h($r['created_at'] ?? '') ?></td>
|
||||
</tr>
|
||||
<?php endforeach; if(!$invRows): ?>
|
||||
<tr><td colspan="4" class="text-muted"><?= h($translations['inv_aucune'] ?? 'Aucune invitation pour le moment.') ?></td></tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-5">
|
||||
<div class="card-body">
|
||||
<h2 class="h6 mb-3"><?= h($translations['inv_secretaires_liees'] ?? 'Secrétaires liées') ?></h2>
|
||||
<div class="table-responsive">
|
||||
<table class="table align-middle mobile-cards" id="table_rdv">
|
||||
<thead><tr>
|
||||
<th>Email</th><th><?= h($translations['droits_gestion_rdv'] ?? 'RDV') ?></th><th><?= h($translations['droits_dispos'] ?? 'Dispos') ?></th><th><?= h($translations['etat'] ?? 'État') ?></th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<?php foreach($secRows as $r): ?>
|
||||
<tr>
|
||||
<td><?= h($r['email']) ?></td>
|
||||
<td>
|
||||
<form method="post" class="d-inline">
|
||||
<input type="hidden" name="csrf" value="<?= h($csrf) ?>">
|
||||
<input type="hidden" name="action" value="toggle_rights">
|
||||
<input type="hidden" name="spid" value="<?= (int)$r['id'] ?>">
|
||||
<input type="hidden" name="field" value="can_manage_rdv">
|
||||
<button class="btn btn-sm <?= $r['can_manage_rdv']?'btn-success':'btn-outline-secondary' ?>">
|
||||
<?= $r['can_manage_rdv'] ? h($translations['actif'] ?? 'Actif') : h($translations['inactif'] ?? 'Inactif') ?>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
<td>
|
||||
<form method="post" class="d-inline">
|
||||
<input type="hidden" name="csrf" value="<?= h($csrf) ?>">
|
||||
<input type="hidden" name="action" value="toggle_rights">
|
||||
<input type="hidden" name="spid" value="<?= (int)$r['id'] ?>">
|
||||
<input type="hidden" name="field" value="can_edit_slots">
|
||||
<button class="btn btn-sm <?= $r['can_edit_slots']?'btn-success':'btn-outline-secondary' ?>">
|
||||
<?= $r['can_edit_slots'] ? h($translations['actif'] ?? 'Actif') : h($translations['inactif'] ?? 'Inactif') ?>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
<td>
|
||||
<form method="post" class="d-inline">
|
||||
<input type="hidden" name="csrf" value="<?= h($csrf) ?>">
|
||||
<input type="hidden" name="action" value="toggle_rights">
|
||||
<input type="hidden" name="spid" value="<?= (int)$r['id'] ?>">
|
||||
<input type="hidden" name="field" value="is_active">
|
||||
<button class="btn btn-sm <?= $r['is_active']?'btn-success':'btn-outline-secondary' ?>">
|
||||
<?= $r['is_active'] ? h($translations['actif'] ?? 'Actif') : h($translations['inactif'] ?? 'Inactif') ?>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; if(!$secRows): ?>
|
||||
<tr><td colspan="4" class="text-muted"><?= h($translations['inv_aucune_secretaire'] ?? 'Aucune secrétaire liée pour le moment.') ?></td></tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script defer>
|
||||
(function(){
|
||||
|
||||
function initMobileCards(tbl){
|
||||
if (!tbl) return;
|
||||
const heads = Array.from(tbl.querySelectorAll('thead th'))
|
||||
.map(th => th.textContent.trim());
|
||||
|
||||
tbl.querySelectorAll('tbody tr').forEach(tr => {
|
||||
Array.from(tr.children).forEach((td, i) => {
|
||||
if (!td.hasAttribute('data-label')) {
|
||||
td.setAttribute('data-label', heads[i] || '');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function ready(fn){
|
||||
if (document.readyState === 'loading')
|
||||
document.addEventListener('DOMContentLoaded', fn);
|
||||
else
|
||||
fn();
|
||||
}
|
||||
|
||||
ready(function(){
|
||||
|
||||
// 👉 liste de toutes les variantes
|
||||
const selectors = [
|
||||
'#table_rdv',
|
||||
'table.mobile-cards',
|
||||
'#table_rdv1',
|
||||
'table.mobile-cards1'
|
||||
];
|
||||
|
||||
// 👉 récupérer toutes les tables existantes dans la page
|
||||
const tables = selectors
|
||||
.map(sel => document.querySelector(sel))
|
||||
.filter(Boolean); // retire les null
|
||||
|
||||
tables.forEach(initMobileCards);
|
||||
|
||||
if (tables.length > 0) {
|
||||
document.documentElement.classList.add('mc-ready');
|
||||
}
|
||||
});
|
||||
|
||||
})();
|
||||
</script>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p><?= $translations['copyr'] ?? '© '.date('Y') ?></p>
|
||||
</div>
|
||||
</footer>
|
||||
@@ -0,0 +1,325 @@
|
||||
<?php
|
||||
// /public/cabinet/rendez_vous.php (VERSION SECRÉTAIRE)
|
||||
require_once __DIR__ . '/../includes/init_secretaire.php';
|
||||
|
||||
|
||||
if (!function_exists('h')) { function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); } }
|
||||
|
||||
// --- Contexte secrétaire + praticien obligatoire
|
||||
$compteId = (int)($_SESSION['compte_id'] ?? 0);
|
||||
$userRole = strtolower((string)($_SESSION['role'] ?? ''));
|
||||
$praticien_id = isset($_GET['praticien_id']) ? (int)$_GET['praticien_id'] : 0;
|
||||
if ($userRole !== 'secretaire' || $praticien_id <= 0) { http_response_code(403); exit('Forbidden'); }
|
||||
|
||||
// --- Vérifier pivot + droit can_manage_rdv
|
||||
$st = $pdo->prepare("
|
||||
SELECT can_manage_rdv
|
||||
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_manage_rdv'] !== 1) { http_response_code(403); exit('Forbidden'); }
|
||||
|
||||
// --- Paramètres
|
||||
$id_rdv = isset($_GET['id']) ? (int)$_GET['id'] : null;
|
||||
|
||||
// --- Suppression (POST) : bornée au praticien + statut 'annulé'
|
||||
if ($id_rdv && isset($_POST['delete'])) {
|
||||
// delete uniquement si le RDV appartient à ce praticien et est annulé
|
||||
$del = $pdo->prepare("DELETE FROM rendez_vous WHERE id = :id AND praticien_id = :p AND statut = 'annulé'");
|
||||
$del->execute(['id'=>$id_rdv,'p'=>$praticien_id]);
|
||||
|
||||
echo '<div class="main1">';
|
||||
echo '<div class="container py-5">';
|
||||
echo ' <div class="shadow p-4 rounded bg-light text-center">';
|
||||
echo ' <div class="alert alert mb-4" role="alert">';
|
||||
echo ' <h4>'.h($translations['rdv_supprime'] ?? 'Rendez-vous supprimé').'</h4>';
|
||||
echo ' </div>';
|
||||
echo ' <a href="/public/cabinet/rendez_vous.php?praticien_id='. (int)$praticien_id .'" class="btn btn-primary">';
|
||||
echo ' <i class="bi bi-arrow-left-circle"></i> '.h($translations['retour_a_la_liste'] ?? 'Retour à la liste').'
|
||||
</a>';
|
||||
echo ' </div>';
|
||||
echo '</div>';
|
||||
echo '</div>';
|
||||
exit;
|
||||
}
|
||||
|
||||
// --- Charger les RDV (bornés au praticien)
|
||||
if ($id_rdv) {
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT rv.*, c.nom AS nom_client, c.prenom AS prenom_client
|
||||
FROM rendez_vous rv
|
||||
JOIN client c ON rv.client_id = c.id
|
||||
WHERE rv.id = :id
|
||||
AND rv.praticien_id = :p
|
||||
LIMIT 1
|
||||
");
|
||||
$stmt->execute(['id'=>$id_rdv,'p'=>$praticien_id]);
|
||||
$one = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
$rdvs = $one ? [$one] : [];
|
||||
} else {
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT rv.*, c.nom AS nom_client, c.prenom AS prenom_client
|
||||
FROM rendez_vous rv
|
||||
JOIN client c ON rv.client_id = c.id
|
||||
WHERE rv.praticien_id = :p
|
||||
ORDER BY rv.date_rdv DESC
|
||||
");
|
||||
$stmt->execute(['p'=>$praticien_id]);
|
||||
$rdvs = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
// --- Map statuts (valeurs BDD -> libellés traduits)
|
||||
$statutMap = [
|
||||
'prévu' => $translations['statut_prevu'] ?? 'Prévu',
|
||||
'confirmé' => $translations['statut_confirme'] ?? 'Confirmé',
|
||||
'annulé' => $translations['statut_annule'] ?? 'Annulé',
|
||||
'terminé' => $translations['statut_termine'] ?? 'Terminé',
|
||||
];
|
||||
|
||||
$PAGE_TITRE = $translations['mes_rdv_title'] ?? 'Mes rendez-vous';
|
||||
$HEAD_EXTRA = <<<HTML
|
||||
<script src="/js/sweetalert2@11.js"></script>
|
||||
HTML;
|
||||
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['vse_consult'] ?? 'Mes rendez-vous') ?></h2>
|
||||
|
||||
<?php if (!$id_rdv): ?>
|
||||
<a href="/public/cabinet/rendez_vous_du_jour.php?praticien_id=<?= (int)$praticien_id ?>" class="btn btn-outline-secondary mb-4">
|
||||
<?= h($translations['voir_rdv_du_jour'] ?? 'Voir les RDV du jour') ?>
|
||||
</a>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="filtre_statut" class="form-label"><?= h($translations['filtrer_par_statut'] ?? 'Filtrer par statut') ?></label>
|
||||
<select id="filtre_statut" class="form-select w-auto d-inline-block" onchange="filtrer()">
|
||||
<option value=""><?= h($translations['option_tous'] ?? 'Tous') ?></option>
|
||||
<?php foreach (['prévu','confirmé','annulé','terminé'] as $s): ?>
|
||||
<option value="<?= $s ?>"><?= h($statutMap[$s] ?? ucfirst($s)) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-striped align-middle text-center mobile-cards" id="table_rdv">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th><?= h($translations['col_date'] ?? 'Date') ?></th>
|
||||
<th><?= h($translations['col_heure'] ?? 'Heure') ?></th>
|
||||
<th><?= h($translations['col_client'] ?? 'Client') ?></th>
|
||||
<th><?= h($translations['col_duree'] ?? 'Durée') ?></th>
|
||||
<th><?= h($translations['col_statut'] ?? 'Statut') ?></th>
|
||||
<?php if (!$id_rdv): ?><th><?= h($translations['col_action'] ?? 'Action') ?></th><?php endif; ?>
|
||||
<!--<th><?= h($translations['btn_chat'] ?? 'Chat') ?></th>
|
||||
<th><?= h($translations['status_online'] ?? 'Visio') ?></th>-->
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (!empty($rdvs)): foreach ($rdvs as $r):
|
||||
$dt = new DateTime($r['date_rdv']);
|
||||
$id = (int)$r['id'];
|
||||
?>
|
||||
<tr data-statut="<?= h($r['statut']) ?>">
|
||||
<td><?= h($dt->format('d/m/Y')) ?></td>
|
||||
<td><?= h($dt->format('H:i')) ?></td>
|
||||
<td><?= h(($r['prenom_client'] ?? '').' '.($r['nom_client'] ?? '')) ?></td>
|
||||
<td><?= (int)$r['duree_minutes'] . ' ' . h($translations['min_abbr'] ?? 'min') ?></td>
|
||||
<td>
|
||||
<select class="form-select form-select-sm" onchange="changerStatut(<?= $id ?>, this.value, <?= (int)$praticien_id ?>)">
|
||||
<?php foreach (['prévu','confirmé','annulé','terminé'] as $s): ?>
|
||||
<option value="<?= $s ?>" <?= ($r['statut'] === $s ? 'selected' : '') ?>>
|
||||
<?= h($statutMap[$s] ?? ucfirst($s)) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</td>
|
||||
|
||||
<?php if (!$id_rdv): ?>
|
||||
<td>
|
||||
<?php if (($r['statut'] ?? '') === 'annulé'): ?>
|
||||
<form method="POST" action="?praticien_id=<?= (int)$praticien_id ?>&id=<?= $id ?>" class="js-del-form d-inline">
|
||||
<input type="hidden" name="delete" value="1">
|
||||
<button type="button" class="btn btn-sm btn-table_del btn-table_del-danger js-del-btn">
|
||||
<?= h($translations['btn_supprimer'] ?? 'Supprimer') ?>
|
||||
</button>
|
||||
</form>
|
||||
<?php else: ?>
|
||||
<a href="?praticien_id=<?= (int)$praticien_id ?>&id=<?= $id ?>" class="btn btn-outline-secondary btn-sm btn-table">
|
||||
<?= h($translations['btn_voir'] ?? 'Voir') ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<?php endif; ?>
|
||||
|
||||
<!--<td>
|
||||
<a class="btn btn-outline-secondary btn-sm btn-table" href="/messages/start.php?client_id=<?= (int)$r['client_id'] ?>">
|
||||
<?= h($translations['chat_praticien'] ?? 'Ouvrir le chat') ?>
|
||||
</a>
|
||||
</td>-->
|
||||
|
||||
<!-- ====== Colonne Visio / Cabinet ======
|
||||
<td>
|
||||
<?php
|
||||
$lieu = strtolower(trim((string)($r['lieu'] ?? '')));
|
||||
if ($lieu === 'online') {
|
||||
$statut = $r['statut'];
|
||||
$tzParis = new DateTimeZone('Europe/Paris');
|
||||
$now = new DateTime('now', $tzParis);
|
||||
$start = new DateTime($r['date_rdv'], $tzParis);
|
||||
$open = (clone $start)->modify('-15 minutes');
|
||||
$close = (clone $start)->modify('+'.(int)($r['duree_minutes'] ?? 120).' minutes');
|
||||
|
||||
if ($statut === 'confirmé') {
|
||||
if ($now >= $open && $now <= $close) {
|
||||
?>
|
||||
<a href="/visio/join.php?rdv=<?= (int)$r['id'] ?>&start=1" class="btn btn-outline-secondary btn-sm btn-table">
|
||||
<?= h($translations['visio_nachalo'] ?? 'Démarrer la visio') ?>
|
||||
</a>
|
||||
<?php
|
||||
} else {
|
||||
?>
|
||||
<small class="text-muted d-block mt-1">
|
||||
<?php
|
||||
if ($now < $open) echo h(($translations['ouvert_a'] ?? 'Ouvert à').' '.$open->format('H:i'));
|
||||
elseif ($now > $close) echo h(($translations['ferme_a'] ?? 'Fermé à').' '.$close->format('H:i'));
|
||||
else echo h(($translations['prev_a'] ?? 'Prévu le').' '.$start->format('d/m/Y \à H:i').'.');
|
||||
?>
|
||||
</small>
|
||||
<?php
|
||||
}
|
||||
} elseif ($statut === 'prévu') {
|
||||
?>
|
||||
<small class="text-danger d-block mt-1"><?= h($translations['votre_rdv_jdet'] ?? 'En attente de confirmation') ?></small>
|
||||
<?php
|
||||
} else {
|
||||
?>
|
||||
<small class="text-muted d-block mt-1"><?= h(($translations['zaverchen_a'] ?? 'Terminé').' '.$close->format('H:i')) ?></small>
|
||||
<?php
|
||||
}
|
||||
} else {
|
||||
echo '<span class="text-nowrap">'.h($translations['option_cabinet'] ?? 'Очно').'</span>';
|
||||
}
|
||||
?>
|
||||
</td>
|
||||
===================================== -->
|
||||
</tr>
|
||||
<?php endforeach; else: ?>
|
||||
<tr>
|
||||
<td colspan="<?= $id_rdv ? 6 : 8 ?>"><?= h($translations['aucun_rdv_trouve'] ?? 'Aucun rendez-vous trouvé') ?></td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<?php if ($id_rdv): ?>
|
||||
<a href="/public/cabinet/rendez_vous.php?praticien_id=<?= (int)$praticien_id ?>" class="btn btn-outline-secondary mb-4">
|
||||
<?= h($translations['retour_a_la_liste'] ?? 'Retour à la liste') ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function changerStatut(id, statut, praticienId) {
|
||||
fetch('/public/cabinet/changer_statut.php', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'},
|
||||
body: new URLSearchParams({ id: String(id), statut: String(statut), praticien_id: String(praticienId) })
|
||||
})
|
||||
.then(async (r) => {
|
||||
const text = await r.text();
|
||||
let data;
|
||||
try { data = JSON.parse(text); }
|
||||
catch { throw new Error('Réponse invalide: ' + text.slice(0,180)); }
|
||||
|
||||
if (!data.ok) throw new Error(data.message || 'Erreur');
|
||||
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: data.message || '<?= addslashes($translations['maj_ok'] ?? "Mise à jour réussie") ?>',
|
||||
timer: 1800,
|
||||
showConfirmButton: false
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: '<?= addslashes($translations['erreur_mise_a_jour'] ?? "Erreur de mise à jour") ?>',
|
||||
text: e.message || '<?= addslashes($translations['erreur_mise_a_jour'] ?? "Erreur de mise à jour.") ?>',
|
||||
confirmButtonText: 'OK'
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
function filtrer() {
|
||||
const valeur = document.getElementById("filtre_statut").value;
|
||||
const lignes = document.querySelectorAll("#table_rdv tbody tr");
|
||||
lignes.forEach(tr => {
|
||||
const statut = tr.dataset.statut;
|
||||
tr.style.display = (!valeur || statut === valeur) ? '' : 'none';
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
document.addEventListener('click', async function(e){
|
||||
const btn = e.target.closest('.js-del-btn');
|
||||
if (!btn) return;
|
||||
const form = btn.closest('.js-del-form');
|
||||
if (!form) return;
|
||||
|
||||
const result = await Swal.fire({
|
||||
icon: 'warning',
|
||||
title: '<?= addslashes($translations["confirm_title"] ?? "Confirmation") ?>',
|
||||
text: '<?= addslashes($translations["confirm_supprimer_rdv"] ?? "Supprimer définitivement ce rendez-vous annulé ?") ?>',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: '<?= addslashes($translations["btn_da_udalit"] ?? "Oui") ?>',
|
||||
cancelButtonText: '<?= addslashes($translations["btn_annuler"] ?? "Annuler") ?>',
|
||||
reverseButtons: true
|
||||
});
|
||||
|
||||
if (result.isConfirmed) form.submit();
|
||||
});
|
||||
</script>
|
||||
|
||||
<script defer>
|
||||
(function(){
|
||||
function initMobileCards(tbl){
|
||||
if (!tbl) return;
|
||||
const heads = Array.from(tbl.querySelectorAll('thead th')).map(th => th.textContent.trim());
|
||||
tbl.querySelectorAll('tbody tr').forEach(tr => {
|
||||
Array.from(tr.children).forEach((td, i) => {
|
||||
if (!td.hasAttribute('data-label')) td.setAttribute('data-label', heads[i] || '');
|
||||
});
|
||||
});
|
||||
document.documentElement.classList.add('mc-ready');
|
||||
}
|
||||
function ready(fn){
|
||||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fn);
|
||||
else fn();
|
||||
}
|
||||
ready(function(){
|
||||
const tbl = document.getElementById('table_rdv') || document.querySelector('table.mobile-cards');
|
||||
initMobileCards(tbl);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p><?= $translations['copyr'] ?? '© '.date('Y') ?></p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,244 @@
|
||||
<?php
|
||||
// /public/cabinet/rendez_vous_du_jour.php
|
||||
require_once __DIR__ . '/../includes/init_secretaire.php';
|
||||
|
||||
|
||||
|
||||
if (!function_exists('h')) { function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); } }
|
||||
|
||||
// --- contexte secrétaire + praticien requis ---
|
||||
$compteId = (int)($_SESSION['compte_id'] ?? 0);
|
||||
$userRole = strtolower((string)($_SESSION['role'] ?? ''));
|
||||
$praticien_id = isset($_GET['praticien_id']) ? (int)$_GET['praticien_id'] : 0;
|
||||
|
||||
if ($userRole !== 'secretaire' || $compteId <= 0 || $praticien_id <= 0) {
|
||||
http_response_code(403); exit('Forbidden');
|
||||
}
|
||||
|
||||
// --- vérification pivot + droit can_manage_rdv ---
|
||||
$st = $pdo->prepare("
|
||||
SELECT can_manage_rdv
|
||||
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_manage_rdv'] !== 1) {
|
||||
http_response_code(403); exit('Forbidden');
|
||||
}
|
||||
|
||||
// --- Aujourd’hui en Europe/Paris ---
|
||||
date_default_timezone_set('Europe/Paris');
|
||||
$tz = new DateTimeZone('Europe/Paris');
|
||||
$todayStart = (new DateTime('today', $tz))->setTime(0, 0, 0);
|
||||
$todayEnd = (clone $todayStart)->setTime(23, 59, 59);
|
||||
$today_start = $todayStart->format('Y-m-d H:i:s');
|
||||
$today_end = $todayEnd->format('Y-m-d H:i:s');
|
||||
|
||||
// --- RDV du jour pour ce praticien ---
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT rv.*, c.nom AS nom_client, c.prenom AS prenom_client, c.email
|
||||
FROM rendez_vous rv
|
||||
JOIN client c ON rv.client_id = c.id
|
||||
WHERE rv.praticien_id = :p
|
||||
AND rv.date_rdv BETWEEN :d1 AND :d2
|
||||
ORDER BY rv.date_rdv ASC
|
||||
");
|
||||
$stmt->execute(['p'=>$praticien_id, 'd1'=>$today_start, 'd2'=>$today_end]);
|
||||
$rdvs = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Map statuts (valeurs BDD -> libellés traduits)
|
||||
$statutMap = [
|
||||
'prévu' => $translations['statut_prevu'] ?? 'Prévu',
|
||||
'confirmé' => $translations['statut_confirme'] ?? 'Confirmé',
|
||||
'annulé' => $translations['statut_annule'] ?? 'Annulé',
|
||||
'terminé' => $translations['statut_termine'] ?? 'Terminé',
|
||||
];
|
||||
|
||||
$PAGE_TITRE = $translations['titre_rdv_du_jour'] ?? 'Rendez-vous du jour';
|
||||
$HEAD_EXTRA = <<<HTML
|
||||
<script src="/js/sweetalert2@11.js"></script>
|
||||
HTML;
|
||||
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['titre_rdv_du_jour'] ?? 'Rendez-vous du jour') ?></h2>
|
||||
|
||||
<a href="/public/cabinet/rendez_vous.php?praticien_id=<?= (int)$praticien_id ?>" class="btn btn-outline-secondary mb-4">
|
||||
<?= h($translations['retour_liste_complete'] ?? 'Retour à la liste complète') ?>
|
||||
</a>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-hover align-middle text-center mobile-cards" id="table_rdv">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th><?= h($translations['col_heure'] ?? 'Heure') ?></th>
|
||||
<th><?= h($translations['col_client'] ?? 'Client') ?></th>
|
||||
<th><?= h($translations['col_duree'] ?? 'Durée') ?></th>
|
||||
<th><?= h($translations['col_statut'] ?? 'Statut') ?></th>
|
||||
<!--<th><?= h($translations['btn_chat'] ?? 'Chat') ?></th>
|
||||
<th><?= h($translations['status_online'] ?? 'Visio') ?></th>-->
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($rdvs as $r):
|
||||
$dt = new DateTime($r['date_rdv'], $tz);
|
||||
$id = (int)$r['id'];
|
||||
?>
|
||||
<tr data-statut="<?= h($r['statut']) ?>">
|
||||
<td><?= h($dt->format('H:i')) ?></td>
|
||||
<td><?= h(($r['prenom_client'] ?? '').' '.($r['nom_client'] ?? '')) ?></td>
|
||||
<td><?= (int)$r['duree_minutes'] . ' ' . h($translations['min_abbr'] ?? 'min') ?></td>
|
||||
<td>
|
||||
<select class="form-select form-select-sm" onchange="changerStatut(<?= $id ?>, this.value, this, <?= (int)$praticien_id ?>)">
|
||||
<?php foreach (['prévu','confirmé','annulé','terminé'] as $s): ?>
|
||||
<option value="<?= $s ?>" <?= ($r['statut'] === $s ? 'selected' : '') ?>>
|
||||
<?= h($statutMap[$s] ?? ucfirst($s)) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</td>
|
||||
|
||||
<!-- Chat -->
|
||||
<!--<td>
|
||||
<a class="btn btn-outline-secondary btn-sm btn-table" href="/messages/start.php?client_id=<?= (int)$r['client_id'] ?>">
|
||||
<?= h($translations['chat_praticien'] ?? 'Ouvrir le chat') ?>
|
||||
</a>
|
||||
</td>-->
|
||||
|
||||
<!-- Visio / Cabinet -->
|
||||
<!--<td>
|
||||
<?php
|
||||
$lieu = strtolower(trim((string)($r['lieu'] ?? '')));
|
||||
if ($lieu === 'online') {
|
||||
$statut = $r['statut']; // 'confirmé' | 'prévu' | 'annulé' | 'terminé'
|
||||
$now = new DateTime('now', $tz);
|
||||
$start = new DateTime($r['date_rdv'], $tz);
|
||||
$open = (clone $start)->modify('-15 minutes');
|
||||
$close = (clone $start)->modify('+'.(int)($r['duree_minutes'] ?? 120).' minutes');
|
||||
|
||||
if ($statut === 'confirmé') {
|
||||
if ($now >= $open && $now <= $close) {
|
||||
?>
|
||||
<a href="/visio/join.php?rdv=<?= (int)$r['id'] ?>&start=1" class="btn btn-outline-secondary btn-sm btn-table">
|
||||
<?= h($translations['visio_nachalo'] ?? 'Démarrer la visio') ?>
|
||||
</a>
|
||||
<?php
|
||||
} else {
|
||||
?>
|
||||
<small class="text-muted d-block mt-1">
|
||||
<?php
|
||||
if ($now < $open) echo h(($translations['ouvert_a'] ?? 'Ouvert à').' '.$open->format('H:i'));
|
||||
elseif ($now > $close) echo h(($translations['ferme_a'] ?? 'Fermé à').' '.$close->format('H:i'));
|
||||
else echo h(($translations['prev_a'] ?? 'Prévu le').' '.$start->format('d/m/Y \à H:i').'.');
|
||||
?>
|
||||
</small>
|
||||
<?php
|
||||
}
|
||||
} elseif ($statut === 'prévu') {
|
||||
?>
|
||||
<small class="text-danger d-block mt-1"><?= h($translations['votre_rdv_jdet'] ?? 'En attente de confirmation') ?></small>
|
||||
<?php
|
||||
} else {
|
||||
?>
|
||||
<small class="text-muted d-block mt-1"><?= h(($translations['zaverchen_a'] ?? 'Terminé').' '.$close->format('H:i')) ?></small>
|
||||
<?php
|
||||
}
|
||||
} else {
|
||||
echo '<span class="text-nowrap">'.h($translations['option_cabinet'] ?? 'Очно').'</span>';
|
||||
}
|
||||
?>
|
||||
</td>-->
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if (empty($rdvs)): ?>
|
||||
<tr>
|
||||
<td colspan="6" class="text-muted">
|
||||
<?= h($translations['aucun_rdv_aujourdhui'] ?? "Aucun rendez-vous aujourd'hui") ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// MAJ statut via endpoint cabinet + praticien_id
|
||||
function changerStatut(id, statut, selEl, praticienId) {
|
||||
fetch('/public/cabinet/changer_statut.php', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'},
|
||||
body: new URLSearchParams({ id: String(id), statut: String(statut), praticien_id: String(praticienId) })
|
||||
})
|
||||
.then(async (r) => {
|
||||
const raw = (await r.text()).trim();
|
||||
let ok = false, message = '';
|
||||
try {
|
||||
const data = JSON.parse(raw);
|
||||
ok = !!data.ok;
|
||||
message = data.message || '';
|
||||
} catch (_) {
|
||||
if (raw.toUpperCase().includes('OK')) { ok = true; message = raw; }
|
||||
else { throw new Error(raw.slice(0, 180)); }
|
||||
}
|
||||
|
||||
if (!ok) throw new Error(message || 'Erreur');
|
||||
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: message || '<?= addslashes($translations['maj_ok'] ?? "Mise à jour réussie") ?>',
|
||||
timer: 1800,
|
||||
showConfirmButton: false
|
||||
});
|
||||
|
||||
try {
|
||||
const tr = selEl && selEl.closest('tr');
|
||||
if (tr) tr.dataset.statut = statut;
|
||||
} catch(e){}
|
||||
})
|
||||
.catch((e) => {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: '<?= addslashes($translations['erreur_mise_a_jour'] ?? "Erreur de mise à jour") ?>',
|
||||
text: e.message || '<?= addslashes($translations['erreur_mise_a_jour'] ?? "Erreur de mise à jour.") ?>',
|
||||
confirmButtonText: 'OK'
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<!--********* Adaptation mobile *********-->
|
||||
<script defer>
|
||||
(function(){
|
||||
function initMobileCards(tbl){
|
||||
if (!tbl) return;
|
||||
const heads = Array.from(tbl.querySelectorAll('thead th')).map(th => th.textContent.trim());
|
||||
tbl.querySelectorAll('tbody tr').forEach(tr => {
|
||||
Array.from(tr.children).forEach((td, i) => {
|
||||
if (!td.hasAttribute('data-label')) {
|
||||
td.setAttribute('data-label', heads[i] || '');
|
||||
}
|
||||
});
|
||||
});
|
||||
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')); });
|
||||
})();
|
||||
</script>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p><?= $translations['copyr'] ?? '© '.date('Y') ?></p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,296 @@
|
||||
<?php
|
||||
// /public/cabinet/reserver_rdv.php
|
||||
require_once __DIR__ . '/../includes/init_secretaire.php';
|
||||
|
||||
// Valeurs par défaut pour les modes
|
||||
$c_online = $c_online ?? 'online';
|
||||
$c_offline = $c_offline ?? 'offline';
|
||||
|
||||
// ---- Entrées ----
|
||||
if (! isset($_GET['praticien_id'])) {
|
||||
exit($translations['praticien_non_precise'] ?? 'Praticien non précisé.');
|
||||
}
|
||||
$praticien_id = (int) $_GET['praticien_id'];
|
||||
$client_id = isset($_GET['client_id']) ? (int) $_GET['client_id'] : null;
|
||||
|
||||
$secretaire_user_id = (int) ($_SESSION['compte_id'] ?? 0);
|
||||
if ($secretaire_user_id <= 0 || $praticien_id <= 0) {http_response_code(403);exit('Forbidden');}
|
||||
|
||||
// ---- Sécurité pivot secrétaire → praticien ----
|
||||
$chk = $pdo->prepare("
|
||||
SELECT can_manage_rdv
|
||||
FROM secretaire_praticien
|
||||
WHERE secretaire_user_id = :u
|
||||
AND praticien_id = :p
|
||||
AND is_active = 1
|
||||
LIMIT 1
|
||||
");
|
||||
$chk->execute([':u' => $secretaire_user_id, ':p' => $praticien_id]);
|
||||
$pivot = $chk->fetch(PDO::FETCH_ASSOC);
|
||||
if (! $pivot) {http_response_code(403);exit('Forbidden');}
|
||||
$canManage = (int) ($pivot['can_manage_rdv'] ?? 0) === 1;
|
||||
|
||||
// ---- Infos praticien ----
|
||||
$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 <= 0) {
|
||||
die($translations['duree_rdv_non_definie'] ?? "Durée de RDV non définie.");
|
||||
}
|
||||
|
||||
// Adresse complète ?
|
||||
$has_address =
|
||||
! empty(trim((string) ($praticien['adresse'] ?? ''))) &&
|
||||
! empty(trim((string) ($praticien['ville'] ?? ''))) &&
|
||||
! empty(trim((string) ($praticien['code_postal'] ?? '')));
|
||||
|
||||
// ---- Dispos praticien ----
|
||||
$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);
|
||||
|
||||
// ---- RDV réservés (prochains 21 jours) ----
|
||||
$tz = new DateTimeZone('Europe/Paris');
|
||||
$now = new DateTimeImmutable('now', $tz);
|
||||
$limit = $now->modify('+60 days');
|
||||
|
||||
$stmt = $pdo->prepare("SELECT date_rdv FROM rendez_vous WHERE praticien_id = ? 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(PDO::FETCH_ASSOC)
|
||||
);
|
||||
|
||||
// ---- i18n jours ----
|
||||
$DAY_I18N = [
|
||||
'monday' => 'jour_lundi',
|
||||
'tuesday' => 'jour_mardi',
|
||||
'wednesday' => 'jour_mercredi',
|
||||
'thursday' => 'jour_jeudi',
|
||||
'friday' => 'jour_vendredi',
|
||||
'saturday' => 'jour_samedi',
|
||||
'sunday' => 'jour_dimanche',
|
||||
];
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// ---- Génération créneaux par jour ----
|
||||
$creneaux_par_jour = [];
|
||||
$periode = new DatePeriod($now, new DateInterval('P1D'), $limit); // $limit exclusif
|
||||
|
||||
foreach ($periode as $jour) {
|
||||
$dayKey = strtolower($jour->format('l'));
|
||||
$i18nKey = $DAY_I18N[$dayKey] ?? null;
|
||||
$jour_nom = $i18nKey ? ($translations[$i18nKey] ?? ucfirst($dayKey)) : ucfirst($dayKey);
|
||||
|
||||
foreach ($dispos as $dispo) {
|
||||
$dispoKey = normalize_day_key($dispo['jour_semaine'] ?? '');
|
||||
if ($dispoKey !== $dayKey) {
|
||||
continue;
|
||||
}
|
||||
|
||||
[$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;
|
||||
}
|
||||
|
||||
$step = new DateInterval("PT{$duree_minutes}M");
|
||||
|
||||
for ($t = $debut_creneau; $t->add($step) <= $fin_creneau; $t = $t->add($step)) {
|
||||
if ($t <= $now) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$debut_str = $t->format('Y-m-d H:i');
|
||||
if (! in_array($debut_str, $reservations, true)) {
|
||||
$key = $jour_nom . '<br> ' . $jour->format('d/m');
|
||||
$creneaux_par_jour[$key][] = $t;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tri horaire dans chaque colonne
|
||||
foreach ($creneaux_par_jour as &$slots) {
|
||||
usort(
|
||||
$slots,
|
||||
fn(DateTimeImmutable $a, DateTimeImmutable $b) => $a->getTimestamp() <=> $b->getTimestamp()
|
||||
);
|
||||
}
|
||||
unset($slots);
|
||||
|
||||
// Pagination colonnes
|
||||
$page = isset($_GET['page']) ? max(1, (int) $_GET['page']) : 1;
|
||||
$limit_columns = 5;
|
||||
$all_keys = array_keys($creneaux_par_jour);
|
||||
$total_pages = max(1, (int) ceil(count($all_keys) / $limit_columns));
|
||||
$offset = ($page - 1) * $limit_columns;
|
||||
$visible_keys = array_slice($all_keys, $offset, $limit_columns);
|
||||
|
||||
// petit helper
|
||||
if (! function_exists('h')) {function h($s)
|
||||
{return htmlspecialchars((string) $s, ENT_QUOTES, 'UTF-8');}}
|
||||
|
||||
$PAGE_TITRE = $translations['creneaux_disponibles_title'] ?? 'Créneaux disponibles';
|
||||
$HEAD_EXTRA = <<<HTML
|
||||
<script src="/js/sweetalert2@11.js"></script>
|
||||
HTML;
|
||||
require_once __DIR__ . '/../includes/header_cabinet.php';
|
||||
?>
|
||||
<div class="main1">
|
||||
<div class="container py-5">
|
||||
<div class="shadow p-4 rounded bg-light rdv-card">
|
||||
<h2 class="mb-4 text-center">
|
||||
<?php echo h(($praticien['prenom'] ?? '') . ' ' . ($praticien['nom'] ?? '')) ?> - <?php echo h($translations['zapis'] ?? 'Prendre rendez-vous') ?>
|
||||
<?php if (! $canManage): ?>
|
||||
<small class="text-danger d-block mt-1"><?php echo h($translations['droits_insuffisants'] ?? 'Droits insuffisants pour réserver') ?></small>
|
||||
<?php endif; ?>
|
||||
</h3>
|
||||
|
||||
<?php if ($total_pages > 1): ?>
|
||||
<div class="pagination mb-3">
|
||||
<?php if ($page > 1): ?>
|
||||
<a class="btn btn-outline-secondary btn-sm"
|
||||
href="?praticien_id=<?php echo (int) $praticien_id ?>&<?php echo $client_id ? 'client_id=' . (int) $client_id . '&' : '' ?>page=<?php echo (int) ($page - 1) ?>">
|
||||
« <?php echo h($translations['pagination_precedent'] ?? 'Précédent') ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($page < $total_pages): ?>
|
||||
<a class="btn btn-outline-secondary btn-sm"
|
||||
href="?praticien_id=<?php echo (int) $praticien_id ?>&<?php echo $client_id ? 'client_id=' . (int) $client_id . '&' : '' ?>page=<?php echo (int) ($page + 1) ?>">
|
||||
<?php echo h($translations['pagination_suivant'] ?? 'Suivant') ?> »
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (empty($creneaux_par_jour)): ?>
|
||||
<p class="rdv-empty">
|
||||
<?php echo h($translations['aucun_creneau_disponible_3_semaines'] ?? 'Aucun créneau disponible sur 3 semaines.') ?><br><br>
|
||||
<a class="btn btn-outline-secondary btn-sm btn-table" href="/messages/start.php?praticien_id=<?php echo (int) $praticien_id ?>">
|
||||
<?php echo h($translations['chat_praticien'] ?? 'Ouvrir le chat') ?>
|
||||
</a>
|
||||
</p>
|
||||
<?php else: ?>
|
||||
<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'] ?? 'В кабинете';
|
||||
|
||||
// URL base vers l’étape de validation (version cabinet)
|
||||
$validerBase = '/public/cabinet/valider_rdv.php';
|
||||
$validerHref = $validerBase
|
||||
. '?praticien_id=' . (int) $praticien_id
|
||||
. '&start=' . rawurlencode($startIso)
|
||||
. ($client_id ? '&client_id=' . (int) $client_id : '');
|
||||
?>
|
||||
<div class="slot">
|
||||
<div class="slot-time"><?php echo h($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 h($slotName) ?>" value="<?php echo h($c_online) ?>" checked>
|
||||
<?php echo h($onlineLabel) ?>
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="<?php echo h($slotName) ?>" value="<?php echo h($c_offline) ?>">
|
||||
<?php echo h($cabinetLabel) ?>
|
||||
</label>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="mode-group my-2"><span><?php echo h($onlineLabel) ?></span></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<a href="<?php echo h($validerHref) ?>"
|
||||
class="reserver-btn"
|
||||
data-mode-name="<?php echo h($has_address ? $slotName : '') ?>"
|
||||
data-default-mode="<?php echo h($c_online) ?>"
|
||||
data-nom="<?php echo h($praticien['nom'] ?? '') ?>"
|
||||
data-prenom="<?php echo h($praticien['prenom'] ?? '') ?>"
|
||||
data-start="<?php echo h($startIso) ?>"
|
||||
<?php echo $canManage ? '' : 'aria-disabled="true" tabindex="-1" style="pointer-events:none;opacity:.6;"' ?>
|
||||
>
|
||||
<?php echo h($translations['btn_reserver'] ?? 'Réserver') ?>
|
||||
</a>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</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;
|
||||
|
||||
// Récupérer le mode choisi
|
||||
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'] ?? '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'] ?? 'Oui, réserver') ?>,
|
||||
cancelButtonText: <?php echo json_encode($translations['btn_annuler'] ?? '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>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p><?php echo $translations['copyr'] ?? '© ' . date('Y') ?></p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,879 @@
|
||||
<?php
|
||||
// /public/cabinet/valider_rdv.php
|
||||
require_once __DIR__ . '/../includes/init_secretaire.php';
|
||||
|
||||
$ROOT = dirname(__DIR__, 2);
|
||||
|
||||
require_once APP_HTDOCS . '/push/push_endpoint.php';
|
||||
// $MAILCFG
|
||||
// (Facultatif) SMS/WhatsApp si tu l'utilises côté client
|
||||
$notifyPath = APP_HTDOCS . '/sms/notify_rdv.php';
|
||||
if (is_file($notifyPath)) require_once $notifyPath;
|
||||
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
|
||||
/* ================== Const / helpers ================== */
|
||||
$c_online = $c_online ?? 'online';
|
||||
$c_offline = $c_offline ?? 'offline';
|
||||
|
||||
if (empty($_SESSION['csrf_token'])) $_SESSION['csrf_token'] = bin2hex(random_bytes(16));
|
||||
$CSRF = $_SESSION['csrf_token'];
|
||||
|
||||
function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
|
||||
function email_norm(string $e): string { return strtolower(trim($e)); }
|
||||
|
||||
/* ------- Mail helpers ------- */
|
||||
function send_mail_ics(
|
||||
string $to, string $toName, string $subject, string $text, string $html,
|
||||
?string $icsName=null, ?string $icsContent=null, ?string $icsMethod='REQUEST'
|
||||
): bool {
|
||||
global $MAILCFG;
|
||||
if (!filter_var($to, FILTER_VALIDATE_EMAIL)) return false;
|
||||
$m = new PHPMailer(true);
|
||||
try {
|
||||
$m->isSMTP();
|
||||
$m->Host = $MAILCFG['smtp']['host']; $m->Port = (int)$MAILCFG['smtp']['port'];
|
||||
$m->SMTPSecure = $MAILCFG['smtp']['secure'] ?? PHPMailer::ENCRYPTION_STARTTLS;
|
||||
$m->SMTPAuth = (bool)($MAILCFG['smtp']['auth'] ?? true);
|
||||
$m->Username = $MAILCFG['smtp']['username'] ?? '';
|
||||
$m->Password = $MAILCFG['smtp']['password'] ?? '';
|
||||
$m->CharSet = 'UTF-8'; $m->Encoding = 'base64'; $m->XMailer = '';
|
||||
if (!empty($MAILCFG['bounce'])) $m->Sender = $MAILCFG['bounce'];
|
||||
$from = $MAILCFG['from'] ?? ['no-reply@uslugi.fr','USLUGI.FR'];
|
||||
$rpto = $MAILCFG['reply_to'] ?? ['contact@uslugi.fr','USLUGI.FR'];
|
||||
$m->setFrom($from[0], $from[1] ?? '');
|
||||
if (!empty($rpto[0])) $m->addReplyTo($rpto[0], $rpto[1] ?? '');
|
||||
$m->addAddress($to, $toName);
|
||||
if (!empty($MAILCFG['suppress_auto'])) {
|
||||
$m->addCustomHeader('Auto-Submitted','auto-generated');
|
||||
$m->addCustomHeader('X-Auto-Response-Suppress','All');
|
||||
}
|
||||
$m->isHTML(true); $m->Subject = $subject; $m->Body = $html; $m->AltBody = $text;
|
||||
if ($icsName && $icsContent !== null && $icsMethod) {
|
||||
$m->addStringAttachment($icsContent, $icsName, 'base64', 'text/calendar; method='.$icsMethod.'; charset=UTF-8');
|
||||
}
|
||||
$m->send(); return true;
|
||||
} catch(Throwable $e){ error_log('send_mail_ics: '.$e->getMessage()); return false; }
|
||||
}
|
||||
|
||||
/* ====== Email template (même rendu pour client/praticien) ====== */
|
||||
function email_tpl_html(array $d, array $t): string {
|
||||
$brand = '<img src="https://uslugi.fr/public/uploads/img/logo_mail.png" width="150" alt="USLUGI.FR">';
|
||||
$brand_bas ='УСЛУГИ.ФР';
|
||||
return '<!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>USLUGI.FR</title></head>
|
||||
<body style="margin:0;padding:0;background:#f4f6f8;font-family:Arial,Helvetica,sans-serif;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f4f6f8;padding:24px 0;"><tr><td align="center">
|
||||
<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="background:#fff;border-radius:8px;overflow:hidden;">
|
||||
<tr><td style="background:#38378B;color:#fff;padding:20px 24px;font-size:18px;font-weight:600;">'.$brand.'</td></tr>
|
||||
<tr><td style="padding:24px 24px 8px 24px;color:#111;font-size:16px;line-height:1.5;">'.nl2br(htmlspecialchars($d['hello'])).'</td></tr>
|
||||
<tr><td style="padding:8px 24px;color:#111;font-size:16px;line-height:1.6;">'.nl2br(htmlspecialchars($d['intro'])).'</td></tr>
|
||||
<tr><td style="padding:16px 24px;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border:1px solid #e5e7eb;border-radius:8px;">
|
||||
<tr><td style="padding:14px 16px;font-size:14px;color:#6b7280;border-bottom:1px solid #e5e7eb;">'.htmlspecialchars($d['who_label']).'</td><td style="padding:14px 16px;font-size:14px;color:#111;">'.htmlspecialchars($d['who_value']).'</td></tr>
|
||||
<tr><td style="padding:14px 16px;font-size:14px;color:#6b7280;">'.htmlspecialchars($t['date_label'] ?? 'Date').'</td><td style="padding:14px 16px;font-size:14px;color:#111;">'.htmlspecialchars($d['when']).'</td></tr>'.
|
||||
(!empty($d['format_line']) ? '<tr><td style="padding:14px 16px;font-size:14px;color:#6b7280;">'.htmlspecialchars($t['format_label'] ?? 'Формат').'</td><td style="padding:14px 16px;font-size:14px;color:#111;">'.htmlspecialchars($d['format_line']).'</td></tr>' : '').
|
||||
(!empty($d['address_line']) ? '<tr><td style="padding:14px 16px;font-size:14px;color:#6b7280;">'.htmlspecialchars($t['adresse_label'] ?? 'Adresse').'</td><td style="padding:14px 16px;font-size:14px;color:#111;">'.htmlspecialchars($d['address_line']).'</td></tr>' : '').
|
||||
'</table>
|
||||
</td></tr>'.
|
||||
(!empty($d['cta_url']) ? '<tr><td align="center" style="padding:8px 24px 24px 24px;"><a href="'.htmlspecialchars($d['cta_url']).'" target="_blank" style="display:inline-block;text-decoration:none;padding:12px 20px;border-radius:999px;background:#E03C3F;color:#fff;font-weight:600;">'.htmlspecialchars($d['cta_label']).'</a></td></tr>' : '').
|
||||
'<tr><td style="padding:8px 24px 24px 24px;color:#111;font-size:16px;line-height:1.6;">'.nl2br(htmlspecialchars($d['footer'])).'</td></tr>
|
||||
</table>
|
||||
<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="margin-top:12px;"><tr><td style="text-align:center;color:#9ca3af;font-size:12px;padding:8px 0;">© '.date('Y').' '.$brand_bas.' — '.htmlspecialchars($t['merci_simple'] ?? 'Merci').'</td></tr></table>
|
||||
</td></tr></table></body></html>';
|
||||
}
|
||||
function email_tpl_text(array $d, array $t): string {
|
||||
$L=[]; $L[]=$d['hello']; $L[]=''; $L[]=$d['intro']; $L[]='';
|
||||
$L[]=($d['who_label']).': '.$d['who_value'];
|
||||
$L[]=($t['date_label'] ?? 'Date').': '.$d['when'];
|
||||
if (!empty($d['format_line'])) $L[]=($t['format_label'] ?? 'Формат').': '.$d['format_line'];
|
||||
if (!empty($d['address_line'])) $L[]=($t['adresse_label'] ?? 'Adresse').': '.$d['address_line'];
|
||||
if (!empty($d['cta_url'])) { $L[]=''; $L[]=($d['cta_label'] ?? 'Voir les détails').': '.$d['cta_url']; }
|
||||
$L[]=''; $L[]=$d['footer']; return implode("\n",$L);
|
||||
}
|
||||
|
||||
|
||||
/* ------- ICS helpers ------- */
|
||||
function ics_escape(string $s): string {
|
||||
$s = str_replace("\\","\\\\",$s);
|
||||
$s = str_replace(";","\;",$s);
|
||||
$s = str_replace(",","\,",$s);
|
||||
return preg_replace("/\r\n|\r|\n/","\\n",$s);
|
||||
}
|
||||
function ics_build(
|
||||
string $uid, DateTime $start, DateTime $end, string $tz, string $summary,
|
||||
string $desc, string $loc, array $attendees, string $method='REQUEST', string $status='CONFIRMED'
|
||||
): string {
|
||||
$L=[];
|
||||
$L[]="BEGIN:VCALENDAR";
|
||||
$L[]="VERSION:2.0";
|
||||
$L[]="PRODID:-//УСЛУГИ.ФР//RDV//FR";
|
||||
$L[]="CALSCALE:GREGORIAN";
|
||||
$L[]="METHOD:{$method}";
|
||||
$L[]="BEGIN:VEVENT";
|
||||
$L[]="UID:{$uid}";
|
||||
$L[]="DTSTAMP:".gmdate('Ymd\THis\Z');
|
||||
$L[]="DTSTART;TZID={$tz}:".$start->format('Ymd\THis');
|
||||
$L[]="DTEND;TZID={$tz}:".$end->format('Ymd\THis');
|
||||
if ($summary) $L[]="SUMMARY:".ics_escape($summary);
|
||||
if ($desc) $L[]="DESCRIPTION:".ics_escape($desc);
|
||||
if ($loc) $L[]="LOCATION:".ics_escape($loc);
|
||||
$L[]="ORGANIZER;CN=УСЛУГИ.ФР:mailto:rdv@uslugi.fr";
|
||||
foreach ($attendees as $a) {
|
||||
if (empty($a['email'])) continue;
|
||||
$cn = ics_escape($a['cn'] ?? '');
|
||||
$L[]="ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;CN={$cn};RSVP=TRUE:mailto:".$a['email'];
|
||||
}
|
||||
$L[]="STATUS:{$status}";
|
||||
$L[]="END:VEVENT";
|
||||
$L[]="END:VCALENDAR";
|
||||
return implode("\r\n",$L)."\r\n";
|
||||
}
|
||||
|
||||
/* ================== Entrées ================== */
|
||||
$secretaire_user_id = (int)($_SESSION['compte_id'] ?? 0);
|
||||
$praticien_id = isset($_GET['praticien_id']) ? (int)$_GET['praticien_id'] : (int)($_POST['praticien_id'] ?? 0);
|
||||
$startIso = $_GET['start'] ?? ($_POST['start'] ?? null);
|
||||
$mode_rdv_in = $_GET['mode_rdv'] ?? ($_POST['mode_rdv'] ?? null);
|
||||
$client_id = isset($_GET['client_id']) ? (int)$_GET['client_id'] : (isset($_POST['client_id']) ? (int)$_POST['client_id'] : 0);
|
||||
|
||||
/* ================== Vérif pivot secrétaire ↔ praticien ================== */
|
||||
$chk = $pdo->prepare("
|
||||
SELECT can_manage_rdv
|
||||
FROM secretaire_praticien
|
||||
WHERE secretaire_user_id = :u
|
||||
AND praticien_id = :p
|
||||
AND is_active = 1
|
||||
LIMIT 1
|
||||
");
|
||||
$chk->execute([':u'=>$secretaire_user_id, ':p'=>$praticien_id]);
|
||||
$pivot = $chk->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$pivot) { http_response_code(403); exit('<div style="padding:24px">Forbidden</div>'); }
|
||||
if (((int)($pivot['can_manage_rdv'] ?? 0)) !== 1) {
|
||||
http_response_code(403);
|
||||
exit('<div style="padding:24px">'.h($translations['erreur_droits_reservation'] ?? 'Droits insuffisants pour réserver').'</div>');
|
||||
}
|
||||
|
||||
/* ================== Récup praticien ================== */
|
||||
$stmt = $pdo->prepare("SELECT nom, prenom, duree_rdv, adresse, ville, code_postal, email FROM praticien WHERE id=?");
|
||||
$stmt->execute([$praticien_id]);
|
||||
$praticien = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$praticien) { http_response_code(404); exit('Praticien introuvable'); }
|
||||
|
||||
$duree = (int)($praticien['duree_rdv'] ?? 60);
|
||||
$has_address =
|
||||
!empty(trim((string)($praticien['adresse'] ?? ''))) &&
|
||||
!empty(trim((string)($praticien['ville'] ?? ''))) &&
|
||||
!empty(trim((string)($praticien['code_postal'] ?? '')));
|
||||
|
||||
/* ================== Mode POST: AJAX (recherche / création / validation) ================== */
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') {
|
||||
|
||||
/* ----- Recherche dynamique de client plateforme non lié ----- */
|
||||
// ----- Recherche dynamique de client plateforme non lié -----
|
||||
if (($_POST['action'] ?? '') === 'search_client') {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
if (!hash_equals($_SESSION['csrf_token'] ?? '', $_POST['csrf'] ?? '')) {
|
||||
echo json_encode(['ok'=>false, 'items'=>[], 'msg'=>'CSRF']); exit;
|
||||
}
|
||||
|
||||
$q = trim((string)($_POST['q'] ?? ''));
|
||||
if ($q === '' || mb_strlen($q) < 2) { // évite spam / vide
|
||||
echo json_encode(['ok'=>true, 'items'=>[]]); exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$like = '%'.$q.'%';
|
||||
|
||||
// NOTE: si votre table client n'a PAS adresse/ville/code_postal,
|
||||
// remplacez la ligne SELECT par: SELECT c.id, c.prenom, c.nom, c.email, c.telephone
|
||||
$sql = "
|
||||
SELECT c.id, c.prenom, c.nom, c.email, c.telephone, c.adresse, c.ville, c.code_postal
|
||||
FROM client c
|
||||
WHERE (
|
||||
c.email LIKE :like_email
|
||||
OR CONCAT(c.prenom,' ',c.nom) LIKE :like_np1
|
||||
OR CONCAT(c.nom,' ',c.prenom) LIKE :like_np2
|
||||
OR c.telephone LIKE :like_tel
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM client_dossiers d
|
||||
WHERE d.client_id = c.id
|
||||
AND d.praticien_id = :pid
|
||||
)
|
||||
ORDER BY c.prenom, c.nom
|
||||
LIMIT 12
|
||||
";
|
||||
|
||||
$s = $pdo->prepare($sql);
|
||||
$s->execute([
|
||||
':like_email' => $like,
|
||||
':like_np1' => $like,
|
||||
':like_np2' => $like,
|
||||
':like_tel' => $like,
|
||||
':pid' => $praticien_id,
|
||||
]);
|
||||
|
||||
$items = $s->fetchAll(PDO::FETCH_ASSOC) ?: [];
|
||||
echo json_encode(['ok'=>true, 'items'=>$items]); exit;
|
||||
|
||||
} catch (Throwable $e) {
|
||||
error_log('search_client: '.$e->getMessage());
|
||||
echo json_encode(['ok'=>false, 'items'=>[], 'msg'=>'Erreur serveur']); exit;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// création client rapide ?
|
||||
if (($_POST['action'] ?? '') === 'create_client') {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
if (!hash_equals($_SESSION['csrf_token'] ?? '', $_POST['csrf'] ?? '')) {
|
||||
http_response_code(400);
|
||||
exit(json_encode(['ok'=>false,'msg'=>'CSRF']));
|
||||
}
|
||||
$nom = trim((string)($_POST['nom'] ?? ''));
|
||||
$prenom = trim((string)($_POST['prenom'] ?? ''));
|
||||
$email = email_norm((string)($_POST['email'] ?? ''));
|
||||
$tel = trim((string)($_POST['telephone'] ?? ''));
|
||||
$dob = trim((string)($_POST['date_naissance'] ?? '')); // YYYY-MM-DD
|
||||
|
||||
if ($nom === '' || $prenom === '' || $email === '' || $tel === '') {
|
||||
exit(json_encode(['ok'=>false,'msg'=>($translations['donnees_manquantes_reservation'] ?? 'Champs manquants')]));
|
||||
}
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
exit(json_encode(['ok'=>false,'msg'=>($translations['email_invalide'] ?? 'Email invalide')]));
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
|
||||
// Si un compte existe déjà pour cet email
|
||||
$acc = $pdo->prepare("SELECT id, role, reference_id FROM compte WHERE email = ? LIMIT 1");
|
||||
$acc->execute([$email]);
|
||||
$exist = $acc->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($exist) {
|
||||
if (($exist['role'] ?? '') !== 'client') {
|
||||
// Email déjà utilisé par praticien/admin/secretaire => refuser création
|
||||
$pdo->rollBack();
|
||||
exit(json_encode(['ok'=>false,'msg'=>($translations['email_deja_utilise'] ?? 'Cet email est déjà utilisé.')]));
|
||||
}
|
||||
// Récupérer le client lié via reference_id
|
||||
$cid = (int)($exist['reference_id'] ?? 0);
|
||||
if ($cid > 0) {
|
||||
// Pas de re-création, on renvoie le client existant pour réserver directement
|
||||
$pdo->commit();
|
||||
exit(json_encode(['ok'=>true,'client_id'=>$cid,'msg'=>($translations['client_existant_utilise'] ?? 'Client existant sélectionné.')]));
|
||||
}
|
||||
// Compte client sans fiche client (rare) -> on crée la fiche et on lie le compte
|
||||
$insC = $pdo->prepare("
|
||||
INSERT INTO client (nom, prenom, email, telephone, date_naissance, accepted_cgu)
|
||||
VALUES (?,?,?,?,?,1)
|
||||
");
|
||||
$insC->execute([$nom, $prenom, $email, $tel, ($dob ?: null)]);
|
||||
$newClientId = (int)$pdo->lastInsertId();
|
||||
|
||||
$upd = $pdo->prepare("UPDATE compte SET reference_id = ? WHERE id = ? LIMIT 1");
|
||||
$upd->execute([$newClientId, (int)$exist['id']]);
|
||||
|
||||
$pdo->commit();
|
||||
exit(json_encode(['ok'=>true,'client_id'=>$newClientId,'msg'=>($translations['client_cree_ok'] ?? 'Client créé.')]));
|
||||
}
|
||||
|
||||
// Sinon : créer la fiche client
|
||||
$ins = $pdo->prepare("
|
||||
INSERT INTO client (nom, prenom, email, telephone, date_naissance, accepted_cgu)
|
||||
VALUES (?,?,?,?,?,1)
|
||||
");
|
||||
$ins->execute([$nom, $prenom, $email, $tel, ($dob ?: null)]);
|
||||
$newClientId = (int)$pdo->lastInsertId();
|
||||
|
||||
// Créer le compte avec MDP provisoire hashé + token de validation email
|
||||
$provisional = 'Uslugi_11992288';
|
||||
$hash = password_hash($provisional, PASSWORD_DEFAULT);
|
||||
$token = bin2hex(random_bytes(32)); // TTL 24h géré côté endpoint (via cree_le ou table dédiée)
|
||||
|
||||
$accIns = $pdo->prepare("
|
||||
INSERT INTO compte (email, mot_de_passe, role, reference_id, token_validation, email_valide, cree_le, modifie_le)
|
||||
VALUES (?,?,?,?,?,0,NOW(),NOW())
|
||||
");
|
||||
$accIns->execute([$email, $hash, 'client', $newClientId, $token]);
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
// Envoyer l'email d'activation (sans ICS)
|
||||
try {
|
||||
$activationUrl = 'https://uslugi.fr/valider_email_cabinet.php?token='.urlencode($token).'&email='.urlencode($email);
|
||||
$subj_a = $translations['activation_compte_sujet'] ?? 'Активируйте ваш аккаунт / Activez votre compte';
|
||||
$helloName = trim($prenom.' '.$nom);
|
||||
|
||||
$dataA = [
|
||||
'hello' => sprintf($translations['mail_valid_bonjour_nom'] ?? 'Bonjour %s', $helloName),
|
||||
'intro' => $translations['activation_compte_intro'] ??
|
||||
"Чтобы завершить создание аккаунта, пожалуйста подтвердите ваш e-mail и задайте новый пароль.\nPour finaliser la création de votre compte, confirmez votre e-mail et définissez votre mot de passe.",
|
||||
'who_label' => $translations['email_label'] ?? 'E-mail',
|
||||
'who_value' => $email,
|
||||
'when' => date('d/m/Y H:i'),
|
||||
'format_line' => '',
|
||||
'address_line'=> '',
|
||||
'cta_url' => $activationUrl,
|
||||
'cta_label' => $translations['placeholder_nouveau_mdp'] ?? 'Définir le mot de passe',
|
||||
'footer' => $translations['merci_simple'] ?? 'Merci.',
|
||||
];
|
||||
$text_a = email_tpl_text($dataA, $translations);
|
||||
$html_a = email_tpl_html($dataA, $translations);
|
||||
|
||||
// Pas de pièce jointe ICS ici
|
||||
send_mail_ics($email, $helloName, $subj_a, $text_a, $html_a, null, null, null);
|
||||
} catch(Throwable $e) {
|
||||
error_log('activation email send failed: '.$e->getMessage());
|
||||
}
|
||||
|
||||
exit(json_encode([
|
||||
'ok' => true,
|
||||
'client_id' => $newClientId,
|
||||
'msg' => ($translations['client_cree_ok'] ?? 'Client créé.')
|
||||
]));
|
||||
} catch (Throwable $e) {
|
||||
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||
error_log('create_client: '.$e->getMessage());
|
||||
exit(json_encode(['ok'=>false,'msg'=>'Erreur']));
|
||||
}
|
||||
}
|
||||
|
||||
// validation RDV
|
||||
if (($_POST['action'] ?? '') === 'validate') {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
if (!hash_equals($_SESSION['csrf_token'] ?? '', $_POST['csrf'] ?? '')) {
|
||||
echo json_encode(['ok'=>false,'html'=>'<div class="msg"><h2>CSRF</h2></div>']); exit;
|
||||
}
|
||||
|
||||
$client_id = (int)($_POST['client_id'] ?? 0);
|
||||
$startIso = $_POST['start'] ?? null;
|
||||
$mode_in = $_POST['mode_rdv'] ?? null;
|
||||
if ($client_id <= 0 || !$startIso || $praticien_id <= 0) {
|
||||
echo json_encode(['ok'=>false,'html'=>'<div class="msg"><h2>'.h($translations['donnees_manquantes_reservation'] ?? 'Données manquantes').'</h2></div>']); exit;
|
||||
}
|
||||
|
||||
// vérifie que le client existe
|
||||
$c = $pdo->prepare("SELECT id, email, nom, prenom, telephone FROM client WHERE id=?");
|
||||
$c->execute([$client_id]);
|
||||
$client = $c->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$client) {
|
||||
echo json_encode(['ok'=>false,'html'=>'<div class="msg"><h2>Client introuvable</h2></div>']); exit;
|
||||
}
|
||||
// Optionnel: lier au praticien ici si tu veux (sinon, tu as dit que ça se fera à la confirmation)
|
||||
// $pl = $pdo->prepare("INSERT IGNORE INTO client_dossiers (praticien_id, client_id, cree_le) VALUES (?,?,NOW())");
|
||||
// $pl->execute([$praticien_id, $client_id]);
|
||||
|
||||
// créneau libre ?
|
||||
$date_sql = (new DateTime($startIso))->format('Y-m-d H:i:s');
|
||||
$dup = $pdo->prepare("SELECT COUNT(*) FROM rendez_vous WHERE praticien_id=? AND date_rdv=?");
|
||||
$dup->execute([$praticien_id, $date_sql]);
|
||||
if ((int)$dup->fetchColumn() > 0) {
|
||||
echo json_encode(['ok'=>false,'html'=>'<div class="msg"><h2>'.h($translations['creneau_deja_reserve'] ?? 'Créneau déjà réservé').'</h2></div>']); exit;
|
||||
}
|
||||
|
||||
// mode / location
|
||||
$mode = ($mode_in === $GLOBALS['c_offline']) ? 'offline' : 'online';
|
||||
if ($mode === 'offline' && !$GLOBALS['has_address']) $mode = 'online';
|
||||
|
||||
// insert RDV (statut 'prévu')
|
||||
$uuid = bin2hex(random_bytes(16));
|
||||
$ins = $pdo->prepare("
|
||||
INSERT INTO rendez_vous (praticien_id, client_id, date_rdv, duree_minutes, lieu, statut, commentaire, uuid_ical, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, 'prévu', '', ?, NOW())
|
||||
");
|
||||
$ins->execute([$praticien_id, $client_id, $date_sql, $duree, $mode, $uuid]);
|
||||
|
||||
// =========================
|
||||
// LOG RDV (rdv_actions_log) : create (secrétaire)
|
||||
// =========================
|
||||
$rdvIdCreated = (int)$pdo->lastInsertId();
|
||||
// ✅ compte_id client + praticien (pour badge counters)
|
||||
$clientCompteId = 0;
|
||||
$praticienCompteId = 0;
|
||||
|
||||
$st = $pdo->prepare("SELECT id FROM compte WHERE role='client' AND reference_id=? LIMIT 1");
|
||||
$st->execute([(int)$client_id]);
|
||||
$clientCompteId = (int)($st->fetchColumn() ?: 0);
|
||||
|
||||
$st = $pdo->prepare("SELECT id FROM compte WHERE role='praticien' AND reference_id=? LIMIT 1");
|
||||
$st->execute([(int)$praticien_id]);
|
||||
$praticienCompteId = (int)($st->fetchColumn() ?: 0);
|
||||
|
||||
// ✅ Badge RDV : +1 pour le client (RDV créé par secrétaire)
|
||||
if ($clientCompteId > 0) {
|
||||
$pdo->prepare("
|
||||
INSERT INTO push_badge_counters (compte_id, msg_unread, rdv_unread)
|
||||
VALUES (:cid, 0, 1)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
rdv_unread = LEAST(99, rdv_unread + 1),
|
||||
updated_at = NOW()
|
||||
")->execute([':cid' => $clientCompteId]);
|
||||
}
|
||||
|
||||
// ✅ Badge RDV : +1 pour le praticien (nouveau RDV à confirmer/voir)
|
||||
if ($praticienCompteId > 0) {
|
||||
$pdo->prepare("
|
||||
INSERT INTO push_badge_counters (compte_id, msg_unread, rdv_unread)
|
||||
VALUES (:pid, 0, 1)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
rdv_unread = LEAST(99, rdv_unread + 1),
|
||||
updated_at = NOW()
|
||||
")->execute([':pid' => $praticienCompteId]);
|
||||
}
|
||||
|
||||
$afterJson = json_encode([
|
||||
'statut' => 'prévu',
|
||||
'date_rdv' => $date_sql,
|
||||
'duree_minutes' => (int)$duree,
|
||||
'lieu' => $mode, // 'online'/'offline'
|
||||
'praticien_id' => (int)$praticien_id,
|
||||
'client_id' => (int)$client_id,
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
$logStmt = $pdo->prepare("
|
||||
INSERT INTO rdv_actions_log
|
||||
(rdv_id, action, by_user_id, by_role, before_json, after_json, created_at)
|
||||
VALUES
|
||||
(?, 'create', ?, 'secretaire', NULL, ?, NOW())
|
||||
");
|
||||
$logStmt->execute([$rdvIdCreated, $secretaire_user_id, $afterJson]);
|
||||
|
||||
// =========================
|
||||
// =========================
|
||||
// PUSH RDV — création (par secrétaire)
|
||||
// =========================
|
||||
try {
|
||||
|
||||
// 🔔 Client : nouveau RDV créé
|
||||
push_rdv_to_client(
|
||||
$pdo,
|
||||
(int)$client_id,
|
||||
$translations['push_rdv_created_title'] ?? 'Новая запись',
|
||||
$translations['push_rdv_created_body'] ?? 'Для вас создана новая запись.',
|
||||
'/public/client/mes_rendez_vous.php',
|
||||
[
|
||||
'rdv_id' => (int)$rdvIdCreated,
|
||||
'action' => 'create',
|
||||
'from_role' => 'secretaire',
|
||||
'praticien_id' => (int)$praticien_id,
|
||||
'client_id' => (int)$client_id,
|
||||
'date_rdv' => $date_sql,
|
||||
]
|
||||
);
|
||||
|
||||
// 🔔 Praticien : nouveau RDV créé par secrétaire
|
||||
push_rdv_to_praticien(
|
||||
$pdo,
|
||||
(int)$praticien_id,
|
||||
$translations['push_rdv_created_praticien_title'] ?? 'Новая запись',
|
||||
$translations['push_rdv_created_praticien_body'] ?? 'Секретарь создал(а) новую запись.',
|
||||
'/public/praticien/rendez_vous.php',
|
||||
[
|
||||
'rdv_id' => (int)$rdvIdCreated,
|
||||
'action' => 'create',
|
||||
'from_role' => 'secretaire',
|
||||
'secretaire_id' => (int)$secretaire_user_id,
|
||||
'praticien_id' => (int)$praticien_id,
|
||||
'client_id' => (int)$client_id,
|
||||
'date_rdv' => $date_sql,
|
||||
]
|
||||
);
|
||||
|
||||
} catch (Throwable $e) {
|
||||
error_log('push rdv create (secretaire) failed: '.$e->getMessage());
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Récup infos complètes pour e-mails
|
||||
$q = $pdo->prepare("
|
||||
SELECT rv.id AS rdv_id, rv.date_rdv, rv.duree_minutes, rv.lieu,
|
||||
p.nom AS nom_praticien, p.prenom AS prenom_praticien, p.email AS email_praticien, p.telephone AS tel_praticien,
|
||||
p.adresse AS p_adresse, p.ville AS p_ville, p.code_postal AS p_cp,
|
||||
c.email AS email_client, c.nom AS nom_client, c.prenom AS prenom_client, c.telephone AS tel_client
|
||||
FROM rendez_vous rv
|
||||
JOIN praticien p ON rv.praticien_id = p.id
|
||||
JOIN client c ON rv.client_id = c.id
|
||||
WHERE rv.uuid_ical = ?
|
||||
LIMIT 1
|
||||
");
|
||||
$q->execute([$uuid]);
|
||||
$rdv = $q->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$rdv) {
|
||||
echo json_encode(['ok'=>false,'html'=>'<div class="msg"><h2>RDV introuvable</h2></div>']); exit;
|
||||
}
|
||||
|
||||
// Construire la réponse UI immédiatement
|
||||
$tz='Europe/Paris'; $tzObj = new DateTimeZone($tz);
|
||||
$start = new DateTime($rdv['date_rdv'], $tzObj);
|
||||
$end = (clone $start)->modify('+'.(int)$rdv['duree_minutes'].' minutes');
|
||||
$when_a = $start->format('d/m/Y') . ' ' . ($translations['a'] ?? 'à') . ' ' . $start->format('H:i');
|
||||
|
||||
$html = '<div class="card-notice" style="background:#fff;border-radius:22px;box-shadow:0 12px 40px rgba(0,0,0,.08);padding:22px;">
|
||||
<h1 class="cn-title" style="margin:0 0 6px">'.h($translations['block_1'] ?? 'Спасибо за запись').'</h1>
|
||||
<div class="cn-date" style="color:#60646c;margin-bottom:8px">'.h($when_a).'</div>
|
||||
<p>'.h($translations['block_2'] ?? 'Ожидает подтверждения').'</p>
|
||||
<div class="actions" style="margin-top:12px">
|
||||
<a class="btn btn-outline" href="/public/cabinet/rendez_vous.php?praticien_id='.(int)$praticien_id.'">'.h($translations['retour_a_la_liste'] ?? 'Retour à la liste').'</a>
|
||||
</div>
|
||||
</div>';
|
||||
|
||||
// envoyer la réponse puis continuer (si FastCGI)
|
||||
session_write_close();
|
||||
echo json_encode(['ok'=>true,'html'=>$html]);
|
||||
if (function_exists('fastcgi_finish_request')) fastcgi_finish_request();
|
||||
ignore_user_abort(true);
|
||||
|
||||
// e-mails + ICS en arrière-plan
|
||||
try {
|
||||
$addr = trim(implode(', ', array_filter([
|
||||
trim((string)$rdv['p_adresse']),
|
||||
trim((string)$rdv['p_cp'].' '.$rdv['p_ville'])
|
||||
])));
|
||||
$is_offline = ($rdv['lieu'] === 'offline');
|
||||
$loc = $is_offline ? ($addr ?: ($translations['option_cabinet'] ?? 'Очно')) : ($translations['option_online'] ?? 'Онлайн');
|
||||
|
||||
$uid = $uuid;
|
||||
$ics_client = ics_build(
|
||||
$uid,$start,$end,$tz,
|
||||
($translations['rdv_avec'] ?? 'Rendez-vous avec').' '.($rdv['prenom_praticien'].' '.$rdv['nom_praticien']),
|
||||
'', $loc,
|
||||
[
|
||||
['cn'=>$rdv['prenom_praticien'].' '.$rdv['nom_praticien'],'email'=>$rdv['email_praticien']],
|
||||
['cn'=>$rdv['prenom_client'].' '.$rdv['nom_client'],'email'=>$rdv['email_client']]
|
||||
],
|
||||
'REQUEST','CONFIRMED'
|
||||
);
|
||||
$ics_prat = ics_build(
|
||||
$uid,$start,$end,$tz,
|
||||
($translations['rdv_avec'] ?? 'Rendez-vous avec').' '.($rdv['prenom_client'].' '.$rdv['nom_client']),
|
||||
'', $loc,
|
||||
[
|
||||
['cn'=>$rdv['prenom_client'].' '.$rdv['nom_client'],'email'=>$rdv['email_client']],
|
||||
['cn'=>$rdv['prenom_praticien'].' '.$rdv['nom_praticien'],'email'=>$rdv['email_praticien']]
|
||||
],
|
||||
'REQUEST','CONFIRMED'
|
||||
);
|
||||
|
||||
$cta_client_url='/public/client/mes_rendez_vous.php';
|
||||
$cta_prat_url ='/public/praticien/rendez_vous.php?id='.(int)$rdv['rdv_id'];
|
||||
$cta_label = $translations['consultez_espace_pour_details'] ?? 'Voir les détails';
|
||||
|
||||
// Client — même template propre que pour le praticien
|
||||
$subj_c = $translations['email_confirmation_rdv_sujet'] ?? 'Confirmation de rendez-vous';
|
||||
|
||||
$dataC = [
|
||||
'hello' => sprintf($translations['mail_valid_bonjour_nom'] ?? 'Bonjour %s',
|
||||
trim(($rdv['prenom_client'] ?? '').' '.($rdv['nom_client'] ?? ''))),
|
||||
'intro' => ($translations['votre_rdv_attente'] ?? 'Votre rendez-vous est en attente de confirmation').' '.
|
||||
($translations['email_avec'] ?? 'avec').' '.
|
||||
trim(($rdv['prenom_praticien'] ?? '').' '.($rdv['nom_praticien'] ?? '')).'.',
|
||||
'who_label' => $translations['praticien_label'] ?? 'Praticien',
|
||||
'who_value' => trim(($rdv['prenom_praticien'] ?? '').' '.($rdv['nom_praticien'] ?? '')),
|
||||
'when' => $when_a,
|
||||
'format_line' => ($rdv['lieu'] === 'offline')
|
||||
? ($translations['option_cabinet'] ?? 'В кабинете')
|
||||
: ($translations['option_online'] ?? 'Онлайн'),
|
||||
'address_line'=> ($rdv['lieu'] === 'offline') ? ($addr ?: '') : '',
|
||||
'cta_url' => $cta_client_url,
|
||||
'cta_label' => $translations['consultez_espace_pour_details'] ?? 'Voir les détails',
|
||||
'footer' => $translations['merci_simple'] ?? 'Merci.',
|
||||
];
|
||||
|
||||
$text_c = email_tpl_text($dataC, $translations);
|
||||
$html_c = email_tpl_html($dataC, $translations);
|
||||
|
||||
send_mail_ics(
|
||||
(string)$rdv['email_client'],
|
||||
(string)trim(($rdv['prenom_client'] ?? '').' '.($rdv['nom_client'] ?? '')),
|
||||
$subj_c, $text_c, $html_c,
|
||||
'rdv_'.$uid.'_client.ics', $ics_client, 'REQUEST'
|
||||
);
|
||||
|
||||
// Praticien
|
||||
$subj_p = ($translations['nouveau_rdv_avec'] ?? 'Nouveau rendez-vous avec').' '.($rdv['prenom_client'].' '.$rdv['nom_client']);
|
||||
|
||||
$dataP = [
|
||||
'hello' => ($translations['email_bonjour_dr'] ?? 'Bonjour').' '.($rdv['prenom_praticien'].' '.$rdv['nom_praticien']),
|
||||
'intro' => ($translations['nouveau_rdv_pris_avec'] ?? 'Un nouveau rendez-vous a été pris.'),
|
||||
'who_label' => $translations['client_label'] ?? 'Client',
|
||||
'who_value' => ($rdv['prenom_client'].' '.$rdv['nom_client']),
|
||||
'when' => $when_a,
|
||||
'format_line' => ($rdv['lieu'] === 'offline')
|
||||
? ($translations['option_cabinet'] ?? 'В кабинете')
|
||||
: ($translations['option_online'] ?? 'Онлайн'),
|
||||
'address_line'=> ($rdv['lieu'] === 'offline') ? ($addr ?: '') : '',
|
||||
'cta_url' => $cta_prat_url,
|
||||
'cta_label' => $cta_label,
|
||||
'footer' => $translations['merci_simple'] ?? 'Merci.',
|
||||
];
|
||||
|
||||
$text_p = email_tpl_text($dataP, $translations);
|
||||
$html_p = email_tpl_html($dataP, $translations);
|
||||
|
||||
send_mail_ics(
|
||||
(string)$rdv['email_praticien'],
|
||||
(string)($rdv['prenom_praticien'].' '.$rdv['nom_praticien']),
|
||||
$subj_p, $text_p, $html_p,
|
||||
'rdv_'.$uid.'_praticien.ics', $ics_prat, 'REQUEST'
|
||||
);
|
||||
|
||||
// WhatsApp côté praticien (optionnel)
|
||||
if (function_exists('Sms\\notify_rdv_en_attente')) {
|
||||
$waTo = (string)($rdv['tel_praticien'] ?? '');
|
||||
if ($waTo) {
|
||||
try { \Sms\notify_rdv_en_attente($waTo, trim($rdv['prenom_client'].' '.$rdv['nom_client']), $start->format('d/m/Y H:i'), (int)$rdv['rdv_id']); }
|
||||
catch(Throwable $e){ error_log('WA notify error: '.$e->getMessage()); }
|
||||
}
|
||||
}
|
||||
} catch(Throwable $e){
|
||||
error_log('cabinet valider_rdv bg: '.$e->getMessage());
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
http_response_code(400);
|
||||
exit('Bad request');
|
||||
}
|
||||
|
||||
/* ================== Mode GET : UI ================== */
|
||||
$title = $translations['titre_validation_secretaire'] ?? 'Validation du rendez-vous (secrétaire)';
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title><?= h($title) ?></title>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<script src="/js/sweetalert2@11.js"></script>
|
||||
<style>
|
||||
body{margin:0;background:#8CA9FF;font-family:system-ui,Segoe UI,Roboto,Arial}
|
||||
.wrap{max-width:720px;margin:32px auto;padding:16px}
|
||||
.card{background:#fff;border-radius:18px;box-shadow:0 12px 36px rgba(0,0,0,.08);padding:20px}
|
||||
.row{display:grid;gap:12px}
|
||||
label{font-weight:600}
|
||||
input,select{width:97%;padding:10px;border:1px solid #e2e8f0;border-radius:10px}
|
||||
.btn{display:inline-block;border-radius:999px;padding:10px 16px;text-decoration:none;font-weight:700}
|
||||
.btn-primary{background:#E03C3F;color:#fff}
|
||||
.btn-outline{background:#fff;border:1px solid #e2e8f0;color:#38378B}
|
||||
.actions{display:flex;gap:10px;flex-wrap:wrap;margin-top:12px}
|
||||
.muted{color:#6b7280}
|
||||
.hint{font-size:12px;color:#6b7280}
|
||||
.pill{display:inline-block;padding:2px 8px;border-radius:999px;border:1px solid #e5e7eb;font-size:12px}
|
||||
.res-item{padding:8px 10px;border:1px solid #e5e7eb;border-radius:10px;margin-bottom:8px;background:#fafafa}
|
||||
.res-item .meta{color:#6b7280;font-size:12px}
|
||||
.err{color:#E03C3F;font-size:12px;display:none}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<div class="card">
|
||||
<h2 style="margin:0 0 8px"><?= h($title) ?></h2>
|
||||
<p class="muted" style="margin:0 0 18px">
|
||||
<?= h(($praticien['prenom'] ?? '').' '.($praticien['nom'] ?? '')) ?> — <?= h($startIso ?: '') ?>
|
||||
</p>
|
||||
|
||||
<?php if (!$praticien_id || !$startIso): ?>
|
||||
<div class="row">
|
||||
<p><?= h($translations['donnees_manquantes_reservation'] ?? 'Données manquantes pour la réservation.') ?></p>
|
||||
<div class="actions">
|
||||
<a class="btn btn-outline" href="/public/cabinet/rendez_vous.php?praticien_id=<?= (int)$praticien_id ?>"><?= h($translations['retour_a_la_liste'] ?? 'Retour à la liste') ?></a>
|
||||
</div>
|
||||
</div>
|
||||
<?php elseif ($client_id > 0): ?>
|
||||
<!-- On a déjà un client -> lance validation directe -->
|
||||
<form id="f-validate" method="post">
|
||||
<input type="hidden" name="csrf" value="<?= h($CSRF) ?>">
|
||||
<input type="hidden" name="action" value="validate">
|
||||
<input type="hidden" name="praticien_id" value="<?= (int)$praticien_id ?>">
|
||||
<input type="hidden" name="start" value="<?= h($startIso) ?>">
|
||||
<input type="hidden" name="client_id" value="<?= (int)$client_id ?>">
|
||||
<input type="hidden" name="mode_rdv" value="<?= h($mode_rdv_in ?? $c_online) ?>">
|
||||
<p class="muted"><?= h($translations['chargement_validation'] ?? 'Подождите, идёт подтверждение...') ?></p>
|
||||
</form>
|
||||
<script>
|
||||
(function(){
|
||||
const f = document.getElementById('f-validate');
|
||||
fetch(location.pathname, {
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/x-www-form-urlencoded; charset=UTF-8'},
|
||||
body: new URLSearchParams(new FormData(f))
|
||||
}).then(r=>r.json()).then(d=>{
|
||||
document.querySelector('.card').innerHTML = d.html || '<p>Erreur</p>';
|
||||
}).catch(()=>{ document.querySelector('.card').innerHTML = '<p>Erreur réseau</p>'; });
|
||||
})();
|
||||
</script>
|
||||
|
||||
<?php else: ?>
|
||||
<!-- Sélection / création client -->
|
||||
<?php
|
||||
// clients déjà liés à ce praticien
|
||||
$cli = $pdo->prepare("
|
||||
SELECT c.id, c.prenom, c.nom, c.email, c.telephone
|
||||
FROM client_dossiers d
|
||||
JOIN client c ON c.id = d.client_id
|
||||
WHERE d.praticien_id = ?
|
||||
ORDER BY c.prenom, c.nom
|
||||
");
|
||||
$cli->execute([$praticien_id]);
|
||||
$clients = $cli->fetchAll(PDO::FETCH_ASSOC);
|
||||
?>
|
||||
<div class="row" style="margin-bottom:16px">
|
||||
<label><?= h($translations['selectionner_client'] ?? 'Sélectionner un client') ?></label>
|
||||
<select id="clientSelect">
|
||||
<option value="0"><?= h($translations['client_existant_placeholder'] ?? '— Choisir un client existant —') ?></option>
|
||||
<?php foreach ($clients as $c): ?>
|
||||
<option value="<?= (int)$c['id'] ?>">
|
||||
<?= h(trim(($c['prenom'] ?? '').' '.($c['nom'] ?? ''))) ?>
|
||||
<?php if(!empty($c['email'])) echo ' — '.h($c['email']); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Recherche globale des clients plateforme non liés à ce praticien -->
|
||||
<div class="row" style="border-top:1px dashed #e2e8f0;padding-top:12px">
|
||||
<label><?= h($translations['recherche_client_plateforme'] ?? 'Rechercher un client (plateforme)') ?></label>
|
||||
<input type="text" id="searchClient" placeholder="<?= h($translations['placeholder_recherche_client'] ?? 'Email, Nom Prénom ou Téléphone') ?>">
|
||||
<div class="hint"><?= h($translations['hint_clients_non_lies'] ?? 'Astuce : les résultats n’incluent pas les clients déjà liés à ce praticien.') ?></div>
|
||||
<div id="searchResults"></div>
|
||||
</div>
|
||||
|
||||
<div class="row" style="border-top:1px dashed #e2e8f0;padding-top:12px">
|
||||
<label><?= h($translations['creer_client'] ?? 'Créer un nouveau client') ?></label>
|
||||
<div class="row">
|
||||
<input type="text" id="newPrenom" placeholder="<?= h($translations['prenom'] ?? 'Prénom') ?>">
|
||||
<input type="text" id="newNom" placeholder="<?= h($translations['nom'] ?? 'Nom') ?>">
|
||||
<input type="email" id="newEmail" placeholder="<?= h($translations['email'] ?? 'Email') ?>">
|
||||
<small id="emailError" class="err"><?= h($translations['email_deja_utilise'] ?? 'Cet email est déjà utilisé.') ?></small>
|
||||
<input type="text" id="newTel" placeholder="<?= h($translations['telephone'] ?? 'Téléphone') ?>">
|
||||
<input type="date" id="newDob" placeholder="<?= h($translations['date_naissance'] ?? 'Date de naissance') ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<a href="#" class="btn btn-primary" id="btn-continue"><?= h($translations['btn_continuer'] ?? 'Continuer') ?></a>
|
||||
<a href="/public/cabinet/rendez_vous.php?praticien_id=<?= (int)$praticien_id ?>" class="btn btn-outline"><?= h($translations['btn_annuler'] ?? 'Annuler') ?></a>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
const csrf = <?= json_encode($CSRF) ?>;
|
||||
const praticien_id = <?= (int)$praticien_id ?>;
|
||||
const start = <?= json_encode($startIso) ?>;
|
||||
const mode_rdv = <?= json_encode($mode_rdv_in ?? $c_online) ?>;
|
||||
|
||||
function validateWith(client_id){
|
||||
const body = new URLSearchParams({ action:'validate', csrf, praticien_id, start, client_id, mode_rdv });
|
||||
fetch(location.pathname, { method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded; charset=UTF-8'}, body })
|
||||
.then(r=>r.json()).then(d=>{
|
||||
document.querySelector('.card').innerHTML = d.html || '<p>Erreur</p>';
|
||||
}).catch(()=>{ Swal.fire('Erreur', 'Réseau', 'error'); });
|
||||
}
|
||||
|
||||
// Recherche dynamique (clients plateforme non liés)
|
||||
const search = document.getElementById('searchClient');
|
||||
const results = document.getElementById('searchResults');
|
||||
let tId = null;
|
||||
function renderResults(items){
|
||||
results.innerHTML = '';
|
||||
if (!items || !items.length) {
|
||||
results.innerHTML = '<div class="hint"><?= h($translations['cl_aucun_resultat'] ?? 'Aucun résultat') ?></div>';
|
||||
return;
|
||||
}
|
||||
items.forEach(it=>{
|
||||
const div = document.createElement('div');
|
||||
div.className = 'res-item';
|
||||
const name = [it.prenom||'', it.nom||''].join(' ').trim();
|
||||
const adr = [it.adresse||'', [it.code_postal||'', it.ville||''].join(' ').trim()].filter(Boolean).join(', ');
|
||||
div.innerHTML =
|
||||
'<div><strong>'+ (name || '—') +'</strong> <span class="pill">'+ (it.email || '—') +'</span></div>'+
|
||||
'<div class="meta"><?= h($translations['telephone'] ?? 'Téléphone') ?>: '+ (it.telephone || '—') + (adr? ' • '+adr : '') +'</div>'+
|
||||
'<div style="margin-top:6px"><button class="btn btn-outline" data-id="'+ it.id +'"><?= h($translations['choisir'] ?? 'Choisir') ?></button></div>';
|
||||
results.appendChild(div);
|
||||
});
|
||||
results.querySelectorAll('button[data-id]').forEach(btn=>{
|
||||
btn.addEventListener('click', (e)=>{
|
||||
e.preventDefault();
|
||||
const id = parseInt(btn.getAttribute('data-id'), 10)||0;
|
||||
if (id>0) validateWith(id);
|
||||
});
|
||||
});
|
||||
}
|
||||
function doSearch(q){
|
||||
if (!q || q.length < 2) { results.innerHTML = ''; return; }
|
||||
const body = new URLSearchParams({ action:'search_client', csrf, praticien_id, q });
|
||||
fetch(location.pathname, { method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded; charset=UTF-8'}, body })
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
if (d.ok) { renderResults(d.items || []); }
|
||||
else { results.innerHTML = '<div class="hint">'+ (d.msg || 'Erreur') +'</div>'; }
|
||||
})
|
||||
.catch(() => { results.innerHTML = '<div class="hint">Erreur réseau</div>'; });
|
||||
}
|
||||
|
||||
search.addEventListener('input', function(){
|
||||
if (tId) clearTimeout(tId);
|
||||
tId = setTimeout(()=>doSearch(search.value.trim()), 250);
|
||||
});
|
||||
|
||||
// Vérif e-mail live (utilise /verifier_email.php à la racine)
|
||||
const emailInput = document.getElementById('newEmail');
|
||||
const emailErr = document.getElementById('emailError');
|
||||
let emailOk = true;
|
||||
function checkEmail(){
|
||||
const v = (emailInput.value||'').trim();
|
||||
emailOk = true;
|
||||
emailErr.style.display = 'none';
|
||||
if (!v.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) return;
|
||||
fetch('/verifier_email.php', {
|
||||
method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded; charset=UTF-8'},
|
||||
body: 'email='+encodeURIComponent(v)
|
||||
}).then(r=>r.text()).then(tx=>{
|
||||
if ((tx||'').trim()==='existe') {
|
||||
emailOk = false;
|
||||
emailErr.style.display = 'block';
|
||||
}
|
||||
}).catch(()=>{ /* silence */ });
|
||||
}
|
||||
emailInput.addEventListener('input', checkEmail);
|
||||
|
||||
document.getElementById('btn-continue').addEventListener('click', function(e){
|
||||
e.preventDefault();
|
||||
const selected = parseInt(document.getElementById('clientSelect').value, 10) || 0;
|
||||
if (selected > 0) return validateWith(selected);
|
||||
|
||||
const prenom = document.getElementById('newPrenom').value.trim();
|
||||
const nom = document.getElementById('newNom').value.trim();
|
||||
const email = document.getElementById('newEmail').value.trim();
|
||||
const tel = document.getElementById('newTel').value.trim();
|
||||
const dob = document.getElementById('newDob').value.trim();
|
||||
|
||||
if (!prenom || !nom || !email || !tel) {
|
||||
Swal.fire('<?= addslashes($translations['donnees_manquantes_reservation'] ?? 'Champs manquants') ?>','','warning');
|
||||
return;
|
||||
}
|
||||
if (!emailOk) {
|
||||
Swal.fire('<?= addslashes($translations['email_deja_utilise'] ?? 'Cet email est déjà utilisé.') ?>','','warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const body = new URLSearchParams({ action:'create_client', csrf, praticien_id, nom, prenom, email, telephone:tel, date_naissance:dob });
|
||||
fetch(location.pathname, { method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded; charset=UTF-8'}, body })
|
||||
.then(r=>r.json()).then(d=>{
|
||||
if (!d.ok) throw new Error(d.msg || 'Erreur');
|
||||
validateWith(d.client_id);
|
||||
}).catch(err=>{
|
||||
Swal.fire('Erreur', err.message || 'Erreur', 'error');
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in new issue
Block a user