Init Git Prod Mars 2026
This commit is contained in:
commit
d81efe93b1
362 files changed
+86627
No files matched your search
@@ -0,0 +1,235 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../includes/init_praticien.php';
|
||||
|
||||
$praticien_id = (int)($_SESSION['reference_id'] ?? 0);
|
||||
$lang = $_SESSION['lang'] ?? 'fr';
|
||||
$fcLocale = ($lang === 'ru') ? 'ru' : 'fr';
|
||||
|
||||
// ✅ ne pas htmlspecialchars ici
|
||||
$PAGE_TITLE = $translations['titre_agenda_page'] ?? 'Agenda';
|
||||
|
||||
$HEAD_EXTRA = <<<HTML
|
||||
<!-- FullCalendar (bundle + toutes les locales) -->
|
||||
<!--<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="/css/sweetalert2.min.css" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
/* ====== Theme léger & responsive ====== */
|
||||
:root{
|
||||
--card-bg:#fff;
|
||||
--muted:#6c757d;
|
||||
--brand:#38378B;
|
||||
--accent:#e9ecef;
|
||||
--text:#222;
|
||||
--border:#e9ecef;
|
||||
}
|
||||
body{ background:#8CA9FF; color:var(--text); }
|
||||
.agenda-wrap{ width:1000px; 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; }
|
||||
.agenda-wrap{ width:750px; max-width:768px; }
|
||||
}
|
||||
</style>
|
||||
HTML;
|
||||
|
||||
require_once __DIR__ . '/../includes/header.php';
|
||||
?>
|
||||
|
||||
<div class="main1">
|
||||
<div class="container py-5">
|
||||
<div class="shadow p-4 rounded bg-light">
|
||||
<h2 class="mb-4 text-center"><?= htmlspecialchars($translations['h2_mon_agenda']) ?></h2>
|
||||
<div id="calendar" role="application" aria-label="calendar"></div>
|
||||
<div id="emptyMsg" class="empty-state" style="display:none;">
|
||||
<?= htmlspecialchars($translations['agenda_aucun_evenement'] ?? 'Aucun rendez-vous à afficher.') ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p><?= $translations['copyr'] ?></p>
|
||||
</div>
|
||||
</footer>
|
||||
<!-- Bootstrap JS -->
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const calendarEl = document.getElementById('calendar');
|
||||
const emptyMsgEl = document.getElementById('emptyMsg');
|
||||
|
||||
// Expose translations to JS
|
||||
const t = <?= json_encode([
|
||||
'rdv_avec' => $translations['sw_rdv_avec'],
|
||||
'date' => $translations['sw_date'],
|
||||
'heure' => $translations['sw_heure'],
|
||||
'statut' => $translations['sw_statut'],
|
||||
'gerer_rdv' => $translations['sw_gerer_rdv'],
|
||||
'btn_fermer' => $translations['sw_btn_fermer'],
|
||||
'err_chargement' => $translations['erreur_chargement_rdv'],
|
||||
'statut_confirme' => $translations['statut_confirme'],
|
||||
'statut_prevu' => $translations['statut_prevu'],
|
||||
'statut_annule' => $translations['statut_annule'],
|
||||
], JSON_UNESCAPED_UNICODE) ?>;
|
||||
|
||||
const fcLocale = <?= json_encode($fcLocale) ?>;
|
||||
|
||||
// Vue par défaut selon la largeur : liste sur mobile, semaine sur desktop
|
||||
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, // Lundi
|
||||
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' // ✅ affiche: Jour | Semaine | Mois
|
||||
: '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') ?>'
|
||||
},
|
||||
// 👉 clé pour corriger "Liste Liste"
|
||||
views: {
|
||||
listDay: { buttonText: '<?= addslashes($translations['btn_jour'] ?? 'Jour') ?>' },
|
||||
listWeek: { buttonText: '<?= addslashes($translations['btn_semaine'] ?? 'Semaine') ?>' }
|
||||
},
|
||||
// Événements
|
||||
events: {
|
||||
url: '/public/praticien/events.php',
|
||||
method: 'POST',
|
||||
extraParams: { praticien_id: <?= json_encode($praticien_id) ?> },
|
||||
failure: function () {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'Oops…',
|
||||
text: t.err_chargement
|
||||
});
|
||||
}
|
||||
},
|
||||
// Ajuste l’état vide
|
||||
loading: function(isLoading) {
|
||||
if (isLoading) return;
|
||||
// Quand le chargement est fini, teste s’il y a des events visibles
|
||||
const hasEvents = calendar.getEvents().length > 0;
|
||||
emptyMsgEl.style.display = hasEvents ? 'none' : 'block';
|
||||
},
|
||||
// Rendu compact des événements
|
||||
eventContent: function(arg){
|
||||
// Sur listView, FullCalendar gère déjà bien. En grille, compactons.
|
||||
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';
|
||||
// arg.timeText -> "10:00 - 10:30"
|
||||
time.textContent = arg.timeText;
|
||||
|
||||
return { domNodes: [title, time] };
|
||||
}
|
||||
return true;
|
||||
},
|
||||
// Modale SweetAlert
|
||||
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/praticien/rendez_vous.php?id=${info.event.id}" target="_blank">${t.gerer_rdv}</a>
|
||||
`,
|
||||
showConfirmButton: true,
|
||||
confirmButtonText: t.btn_fermer
|
||||
});
|
||||
},
|
||||
// Accessibilité basique
|
||||
eventDidMount: function(info){
|
||||
info.el.setAttribute('aria-label', info.event.title);
|
||||
},
|
||||
// Switch auto si on redimensionne
|
||||
windowResize: function(view){
|
||||
const mobile = window.matchMedia('(max-width: 768px)').matches;
|
||||
calendar.setOption('height', mobile ? 'auto' : 680);
|
||||
// Garde la vue “liste” sur mobile si l’utilisateur n’a pas changé manuellement
|
||||
if (mobile && !calendar.view.type.startsWith('list')) {
|
||||
calendar.changeView('listWeek');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
calendar.render();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,649 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../includes/init_praticien.php';
|
||||
require_once __DIR__ . '/../../push/push_endpoint.php';
|
||||
|
||||
|
||||
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);
|
||||
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 error: '.$e->getMessage().($m->ErrorInfo ? ' | '.$m->ErrorInfo : ''));
|
||||
// retry sans ICS si c'est la PJ
|
||||
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()];
|
||||
}
|
||||
}
|
||||
|
||||
// =================== Contrôles d’entrée ===================
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') out(false, 'Méthode invalide.');
|
||||
|
||||
$praticien_id = $_SESSION['reference_id'] ?? null;
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$statut = (string)($_POST['statut'] ?? '');
|
||||
|
||||
$whitelist = ['prévu','confirmé','annulé','terminé'];
|
||||
if (!$praticien_id || $id <= 0 || !in_array($statut, $whitelist, true)) {
|
||||
out(false, $translations['erreur_mise_a_jour'] ?? 'Erreur de mise à jour.');
|
||||
}
|
||||
|
||||
// =================== Update DB sécurisé ===================
|
||||
try {
|
||||
|
||||
// =========================
|
||||
// SNAPSHOT AVANT UPDATE_STATUS (pour log)
|
||||
// =========================
|
||||
$beforeStmt = $pdo->prepare("
|
||||
SELECT statut, date_rdv, duree_minutes, lieu
|
||||
FROM rendez_vous
|
||||
WHERE id = :id AND praticien_id = :pid
|
||||
LIMIT 1
|
||||
");
|
||||
$beforeStmt->execute([':id'=>$id, ':pid'=>$praticien_id]);
|
||||
$before = $beforeStmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$before) {
|
||||
out(false, $translations['erreur_mise_a_jour'] ?? 'Erreur de mise à jour.');
|
||||
}
|
||||
|
||||
// Mise à jour statut
|
||||
|
||||
$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
|
||||
// =========================
|
||||
$beforeJson = json_encode([
|
||||
'statut' => $before['statut'],
|
||||
'date_rdv' => $before['date_rdv'],
|
||||
'duree_minutes' => (int)$before['duree_minutes'],
|
||||
'lieu' => $before['lieu'],
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
$afterJson = json_encode([
|
||||
'statut' => $statut,
|
||||
'date_rdv' => $before['date_rdv'],
|
||||
], 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
|
||||
(:rid, 'update_status', :uid, 'praticien', :b, :a, NOW())
|
||||
");
|
||||
$logStmt->execute([
|
||||
':rid' => (int)$id,
|
||||
':uid' => (int)$praticien_id,
|
||||
':b' => $beforeJson,
|
||||
':a' => $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);
|
||||
|
||||
|
||||
// AJOUT DE GARDE FOUS POUR CLIENT RENDEZ_VOUS
|
||||
if (empty($rdv['client_id'])) {
|
||||
error_log('changer_statut: client_id manquant pour RDV id='.$id);
|
||||
}
|
||||
// FIN DE GARDE FOUS
|
||||
|
||||
|
||||
if (!$rdv) out(false, $translations['erreur_mise_a_jour'] ?? 'Erreur de mise à jour.');
|
||||
|
||||
// ✅ compte_id du client (pour compteur badge rdv)
|
||||
$clientCompteId = 0;
|
||||
if (!empty($rdv['client_id'])) {
|
||||
$st = $pdo->prepare("SELECT id FROM compte WHERE role='client' AND reference_id=? LIMIT 1");
|
||||
$st->execute([(int)$rdv['client_id']]);
|
||||
$clientCompteId = (int)($st->fetchColumn() ?: 0);
|
||||
}
|
||||
|
||||
// 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 = $rdv['prenom_client'].' '.$rdv['nom_client'];
|
||||
$pratFull = $rdv['prenom_praticien'].' '.$rdv['nom_praticien'];
|
||||
|
||||
// ====== FORMAT (Онлайн/Очно + adresse si dispo) ======
|
||||
$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 (Онлайн OU adresse, sinon Очно)
|
||||
if ($lieu === 'online') {
|
||||
$icsLocation = $onlineLabel; // "Онлайн"
|
||||
} elseif ($lieu === 'offline') {
|
||||
$icsLocation = $addr !== '' ? $addr : $cabinetLabel; // adresse si dispo, sinon "Очно"
|
||||
} else {
|
||||
$icsLocation = ''; // fallback neutre
|
||||
}
|
||||
// ======================================================
|
||||
|
||||
$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;
|
||||
|
||||
// ----- envois mails selon statut -----
|
||||
$warnings=[];
|
||||
|
||||
// =====NOUVEAU BLOCK POUR CONTROLER LEZ RENDEZ-VOUS==============
|
||||
//***************************************************************** */
|
||||
// ===== HOOK SUIVI CLIENT — création/MAJ dossier + entrée auto =====
|
||||
|
||||
if ($statut === 'confirmé') {
|
||||
// =========================
|
||||
// PUSH RDV — confirmé (par praticien)
|
||||
// =========================
|
||||
try {
|
||||
// ✅ Badge RDV : +1 pour le client (confirm)
|
||||
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]);
|
||||
}
|
||||
|
||||
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' => 'praticien',
|
||||
'praticien_id' => (int)$praticien_id,
|
||||
'date_rdv' => $rdv['date_rdv'],
|
||||
]
|
||||
);
|
||||
|
||||
} catch (Throwable $e) {
|
||||
error_log('push rdv confirm failed: '.$e->getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
|
||||
// 1) 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();
|
||||
}
|
||||
|
||||
// 2) Créer entrée de suivi automatique (une fois par RDV confirmé)
|
||||
// On évite les doublons si reconfirmation
|
||||
$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é) failed: '.$ex->getMessage());
|
||||
// on n’interrompt pas le flux principal (mails etc.)
|
||||
}
|
||||
// ================================================
|
||||
// ===== FIN HOOK INSERTION CLIENT POUR SUIVIE=====
|
||||
//=================================================
|
||||
|
||||
// Client
|
||||
$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.',
|
||||
];
|
||||
// Ajout format dans le corps du mail (intro)
|
||||
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';
|
||||
|
||||
// Praticien
|
||||
$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.',
|
||||
];
|
||||
// Ajout format pour le praticien
|
||||
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 praticien)
|
||||
// =========================
|
||||
try {
|
||||
// ✅ Badge RDV : +1 pour le client (cancel)
|
||||
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]);
|
||||
}
|
||||
|
||||
push_rdv_to_client(
|
||||
$pdo,
|
||||
(int)$rdv['client_id'],
|
||||
$translations['push_rdv_cancel_praticien_title'] ?? 'Запись отменена',
|
||||
$translations['push_rdv_cancel_praticien_body'] ?? 'Специалист отменил запись.',
|
||||
'/public/client/mes_rendez_vous.php',
|
||||
[
|
||||
'rdv_id' => (int)$rdv['id'],
|
||||
'action' => 'cancel',
|
||||
'from_role' => 'praticien',
|
||||
'praticien_id' => (int)$praticien_id,
|
||||
'date_rdv' => $rdv['date_rdv'],
|
||||
]
|
||||
);
|
||||
|
||||
} catch (Throwable $e) {
|
||||
error_log('push rdv cancel (praticien) 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 CANCEL — on met aussi la LOCATION correcte pour cohérence
|
||||
$ics = !empty($rdv['uuid_ical'])
|
||||
? ics_build($rdv['uuid_ical'],$start,$end,$tzStr,$summary,'',$icsLocation, $attBoth,'CANCEL','CANCELLED')
|
||||
: null;
|
||||
|
||||
// Client
|
||||
$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';
|
||||
|
||||
// Praticien
|
||||
$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é
|
||||
// =========================
|
||||
try {
|
||||
// ✅ Badge RDV : +1 pour le client (done)
|
||||
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]);
|
||||
}
|
||||
|
||||
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' => 'praticien',
|
||||
'praticien_id' => (int)$praticien_id,
|
||||
]
|
||||
);
|
||||
|
||||
} catch (Throwable $e) {
|
||||
error_log('push rdv done failed: '.$e->getMessage());
|
||||
}
|
||||
|
||||
// Client
|
||||
$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/avis.php?praticien_id='.(int)$praticien_id,
|
||||
'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';
|
||||
|
||||
// Praticien
|
||||
$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 fatal: '.$e->getMessage());
|
||||
out(false, $translations['erreur_mise_a_jour'] ?? 'Erreur de mise à jour.');
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../includes/init_praticien.php';
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
function out($ok,$msg,$extra=[]){ echo json_encode(['ok'=>$ok,'message'=>$msg]+$extra); exit; }
|
||||
|
||||
$praticien_id = (int)($_SESSION['reference_id'] ?? 0);
|
||||
$csrf = (string)($_POST['csrf'] ?? '');
|
||||
if (!hash_equals($_SESSION['cl_csrf'] ?? '', $csrf)) out(false, 'CSRF');
|
||||
|
||||
$dossier_id = (int)($_POST['dossier_id'] ?? 0);
|
||||
if ($dossier_id<=0) out(false, 'Bad dossier');
|
||||
|
||||
// Owner
|
||||
$st = $pdo->prepare("SELECT d.id, d.client_id FROM client_dossiers d WHERE d.id=:d AND d.praticien_id=:p AND d.archived_at IS NULL");
|
||||
$st->execute([':d'=>$dossier_id, ':p'=>$praticien_id]);
|
||||
$dos = $st->fetch(PDO::FETCH_ASSOC);
|
||||
if(!$dos) out(false, 'Not allowed');
|
||||
|
||||
$date_rdv = (string)($_POST['date_rdv'] ?? '');
|
||||
$type_rdv = (string)($_POST['type_rdv'] ?? 'autre');
|
||||
$titre = trim((string)($_POST['titre'] ?? ''));
|
||||
$notes = trim((string)($_POST['notes'] ?? ''));
|
||||
$prochaine= trim((string)($_POST['prochaine_action'] ?? ''));
|
||||
$duree = (int)($_POST['duree_minutes'] ?? 0);
|
||||
|
||||
if ($titre==='' || $notes==='') out(false, 'Champs requis');
|
||||
|
||||
try{
|
||||
$pdo->beginTransaction();
|
||||
$ins = $pdo->prepare("INSERT INTO client_suivi (dossier_id, rdv_id, date_rdv, type_rdv, titre, notes, prochaine_action, duree_minutes, prive)
|
||||
VALUES (:d, NULL, :dt, :ty, :ti, :no, :pa, :du, 1)");
|
||||
$ins->execute([
|
||||
':d'=>$dossier_id,
|
||||
':dt'=> (new DateTime($date_rdv ?: 'now'))->format('Y-m-d H:i:s'),
|
||||
':ty'=> in_array($type_rdv,['online','cabinet','tel','autre'],true) ? $type_rdv : 'autre',
|
||||
':ti'=> $titre,
|
||||
':no'=> $notes,
|
||||
':pa'=> ($prochaine !== '' ? $prochaine : null),
|
||||
':du'=> ($duree > 0 ? $duree : null),
|
||||
]);
|
||||
$entry_id = (int)$pdo->lastInsertId();
|
||||
|
||||
// Upload files
|
||||
if (!empty($_FILES['files']) && is_array($_FILES['files']['name'])) {
|
||||
$baseDir = $_SERVER['DOCUMENT_ROOT'].'/uploads/suivi/'. $dossier_id;
|
||||
if (!is_dir($baseDir)) @mkdir($baseDir, 0775, true);
|
||||
|
||||
$allowed = ['pdf','jpg','jpeg','png','doc','docx','txt'];
|
||||
for($i=0;$i<count($_FILES['files']['name']);$i++){
|
||||
$err = (int)($_FILES['files']['error'][$i] ?? UPLOAD_ERR_NO_FILE);
|
||||
if ($err !== UPLOAD_ERR_OK) continue;
|
||||
$name = (string)($_FILES['files']['name'][$i] ?? '');
|
||||
$tmp = (string)($_FILES['files']['tmp_name'][$i] ?? '');
|
||||
$size = (int)($_FILES['files']['size'][$i] ?? 0);
|
||||
if ($size <= 0 || $size > 8*1024*1024) continue; // 8 Mo
|
||||
$ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
|
||||
if (!in_array($ext,$allowed,true)) continue;
|
||||
|
||||
$clean = preg_replace('~[^a-zA-Z0-9._-]+~','_', $name);
|
||||
$new = $dossier_id.'_'.time().'_'.bin2hex(random_bytes(4)).'.'.$ext;
|
||||
$dest = $baseDir.'/'.$new;
|
||||
if (@move_uploaded_file($tmp, $dest)) {
|
||||
$rel = '/uploads/suivi/'.$dossier_id.'/'.$new;
|
||||
$mime= mime_content_type($dest) ?: null;
|
||||
|
||||
$pdo->prepare("INSERT INTO client_suivi_files (entry_id, path, mime, size_bytes) VALUES (:e,:p,:m,:s)")
|
||||
->execute([':e'=>$entry_id, ':p'=>$rel, ':m'=>$mime, ':s'=>$size]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
out(true, $translations['cl_enregistre_ok'] ?? 'Note enregistrée.');
|
||||
}catch(Throwable $e){
|
||||
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||
error_log('ajax_ajouter_note: '.$e->getMessage());
|
||||
out(false, $translations['cl_enregistre_ko'] ?? 'Erreur lors de l’enregistrement.');
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../includes/init_praticien.php';
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
function out($ok,$m,$x=[]){ echo json_encode(['ok'=>$ok,'message'=>$m]+$x); exit; }
|
||||
|
||||
$praticien_id = (int)($_SESSION['reference_id'] ?? 0);
|
||||
$csrf=(string)($_POST['csrf'] ?? '');
|
||||
if (!hash_equals($_SESSION['cl_csrf'] ?? '', $csrf)) out(false,'CSRF');
|
||||
|
||||
$note_id = (int)($_POST['note_id'] ?? 0);
|
||||
if ($note_id<=0) out(false,'Bad note');
|
||||
|
||||
// Vérif propriétaire + dossier_id
|
||||
$sql = "SELECT s.id, s.dossier_id, d.praticien_id
|
||||
FROM client_suivi s JOIN client_dossiers d ON d.id=s.dossier_id
|
||||
WHERE s.id=:id AND d.praticien_id=:p AND d.archived_at IS NULL";
|
||||
$st = $pdo->prepare($sql);
|
||||
$st->execute([':id'=>$note_id, ':p'=>$praticien_id]);
|
||||
$own = $st->fetch(PDO::FETCH_ASSOC);
|
||||
if(!$own) out(false,'Not allowed');
|
||||
|
||||
$date_rdv = (string)($_POST['date_rdv'] ?? '');
|
||||
$type_rdv = (string)($_POST['type_rdv'] ?? 'autre');
|
||||
$titre = trim((string)($_POST['titre'] ?? ''));
|
||||
$notes = trim((string)($_POST['notes'] ?? ''));
|
||||
$prochaine= trim((string)($_POST['prochaine_action'] ?? ''));
|
||||
$duree = (int)($_POST['duree_minutes'] ?? 0);
|
||||
|
||||
if ($titre==='' || $notes==='') out(false,'Champs requis');
|
||||
|
||||
try{
|
||||
$pdo->beginTransaction();
|
||||
|
||||
$upd = $pdo->prepare("UPDATE client_suivi
|
||||
SET date_rdv=:dt, type_rdv=:ty, titre=:ti, notes=:no, prochaine_action=:pa, duree_minutes=:du
|
||||
WHERE id=:id");
|
||||
$upd->execute([
|
||||
':dt'=> (new DateTime($date_rdv ?: 'now'))->format('Y-m-d H:i:s'),
|
||||
':ty'=> in_array($type_rdv,['online','cabinet','tel','autre'],true) ? $type_rdv : 'autre',
|
||||
':ti'=> $titre, ':no'=> $notes,
|
||||
':pa'=> ($prochaine !== '' ? $prochaine : null),
|
||||
':du'=> ($duree > 0 ? $duree : null),
|
||||
':id'=> $note_id
|
||||
]);
|
||||
|
||||
// Uploads additionnels
|
||||
if (!empty($_FILES['files']) && is_array($_FILES['files']['name'])) {
|
||||
$baseDir = $_SERVER['DOCUMENT_ROOT'].'/uploads/suivi/'. (int)$own['dossier_id'];
|
||||
if (!is_dir($baseDir)) @mkdir($baseDir, 0775, true);
|
||||
|
||||
$allowed = ['pdf','jpg','jpeg','png','doc','docx','txt'];
|
||||
for($i=0;$i<count($_FILES['files']['name']);$i++){
|
||||
$err = (int)($_FILES['files']['error'][$i] ?? UPLOAD_ERR_NO_FILE);
|
||||
if ($err !== UPLOAD_ERR_OK) continue;
|
||||
$name = (string)($_FILES['files']['name'][$i] ?? '');
|
||||
$tmp = (string)($_FILES['files']['tmp_name'][$i] ?? '');
|
||||
$size = (int)($_FILES['files']['size'][$i] ?? 0);
|
||||
if ($size <= 0 || $size > 8*1024*1024) continue; // 8 Mo
|
||||
$ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
|
||||
if (!in_array($ext,$allowed,true)) continue;
|
||||
|
||||
$new = (int)$own['dossier_id'].'_'.time().'_'.bin2hex(random_bytes(4)).'.'.$ext;
|
||||
$dest = $baseDir.'/'.$new;
|
||||
if (@move_uploaded_file($tmp, $dest)) {
|
||||
$rel = '/uploads/suivi/'.(int)$own['dossier_id'].'/'.$new;
|
||||
$mime= mime_content_type($dest) ?: null;
|
||||
|
||||
$pdo->prepare("INSERT INTO client_suivi_files (entry_id, path, mime, size_bytes) VALUES (:e,:p,:m,:s)")
|
||||
->execute([':e'=>$note_id, ':p'=>$rel, ':m'=>$mime, ':s'=>$size]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
out(true,'OK');
|
||||
}catch(Throwable $e){
|
||||
if($pdo->inTransaction()) $pdo->rollBack();
|
||||
error_log('ajax_editer_note: '.$e->getMessage());
|
||||
out(false,'Erreur');
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../includes/init_praticien.php';
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
|
||||
header('Pragma: no-cache');
|
||||
|
||||
$praticien_id = (int)($_SESSION['reference_id'] ?? 0);
|
||||
if ($praticien_id <= 0) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['ok'=>false,'message'=>'Unauthorized']); exit;
|
||||
}
|
||||
|
||||
$qRaw = trim((string)($_GET['q'] ?? ''));
|
||||
$hasQ = ($qRaw !== '');
|
||||
$qLike = '%'.$qRaw.'%';
|
||||
|
||||
$page = max(1, (int)($_GET['page'] ?? 1));
|
||||
$limit = 10;
|
||||
$offset= ($page-1)*$limit;
|
||||
|
||||
try {
|
||||
// ----- TOTAL
|
||||
$sqlTotal = "SELECT COUNT(*) FROM client_dossiers d
|
||||
JOIN client c ON c.id=d.client_id
|
||||
WHERE d.praticien_id=:p AND d.archived_at IS NULL";
|
||||
$paramsT = [':p'=>$praticien_id];
|
||||
if ($hasQ) {
|
||||
$sqlTotal .= " AND (
|
||||
c.nom LIKE :q_nom
|
||||
OR c.prenom LIKE :q_prenom
|
||||
OR c.email LIKE :q_email
|
||||
OR c.telephone LIKE :q_tel
|
||||
OR d.tags LIKE :q_tags
|
||||
)";
|
||||
$paramsT[':q_nom'] = $qLike;
|
||||
$paramsT[':q_prenom'] = $qLike;
|
||||
$paramsT[':q_email'] = $qLike;
|
||||
$paramsT[':q_tel'] = $qLike;
|
||||
$paramsT[':q_tags'] = $qLike;
|
||||
}
|
||||
|
||||
$stT = $pdo->prepare($sqlTotal);
|
||||
foreach ($paramsT as $k=>$v) {
|
||||
$stT->bindValue($k, $v, $k===':p' ? PDO::PARAM_INT : PDO::PARAM_STR);
|
||||
}
|
||||
$stT->execute();
|
||||
$total = (int)$stT->fetchColumn();
|
||||
|
||||
// ----- PAGE
|
||||
$sql = "SELECT d.id AS dossier_id, d.tags,
|
||||
c.nom, c.prenom, c.email, c.telephone,
|
||||
(SELECT MAX(rv.date_rdv) FROM rendez_vous rv
|
||||
WHERE rv.client_id=d.client_id AND rv.praticien_id=d.praticien_id
|
||||
AND rv.statut IN ('confirmé','terminé')) AS last_rdv,
|
||||
(SELECT LEFT(s.prochaine_action, 120) FROM client_suivi s
|
||||
WHERE s.dossier_id=d.id ORDER BY s.date_rdv DESC, s.id DESC LIMIT 1) AS last_note
|
||||
FROM client_dossiers d
|
||||
JOIN client c ON c.id=d.client_id
|
||||
WHERE d.praticien_id=:p AND d.archived_at IS NULL";
|
||||
$params = [':p'=>$praticien_id];
|
||||
if ($hasQ) {
|
||||
$sql .= " AND (
|
||||
c.nom LIKE :q_nom
|
||||
OR c.prenom LIKE :q_prenom
|
||||
OR c.email LIKE :q_email
|
||||
OR c.telephone LIKE :q_tel
|
||||
OR d.tags LIKE :q_tags
|
||||
)";
|
||||
$params[':q_nom'] = $qLike;
|
||||
$params[':q_prenom'] = $qLike;
|
||||
$params[':q_email'] = $qLike;
|
||||
$params[':q_tel'] = $qLike;
|
||||
$params[':q_tags'] = $qLike;
|
||||
}
|
||||
|
||||
// tri: d'abord ceux qui ont un last_rdv, puis par date desc, puis maj dossier
|
||||
$sql .= " ORDER BY (last_rdv IS NULL) ASC, last_rdv DESC, d.updated_at DESC
|
||||
LIMIT :lim OFFSET :off";
|
||||
|
||||
$st = $pdo->prepare($sql);
|
||||
foreach ($params as $k=>$v) {
|
||||
if ($k === ':p') $st->bindValue($k, $v, PDO::PARAM_INT);
|
||||
else $st->bindValue($k, $v, PDO::PARAM_STR);
|
||||
}
|
||||
$st->bindValue(':lim', $limit, PDO::PARAM_INT);
|
||||
$st->bindValue(':off', $offset, PDO::PARAM_INT);
|
||||
$st->execute();
|
||||
|
||||
$items=[];
|
||||
while($r=$st->fetch(PDO::FETCH_ASSOC)){
|
||||
$items[]=[
|
||||
'dossier_id' => (int)$r['dossier_id'],
|
||||
'tags' => $r['tags'],
|
||||
'client_name' => trim(($r['prenom'] ?? '').' '.($r['nom'] ?? '')),
|
||||
'email' => $r['email'] ?? '',
|
||||
'telephone' => $r['telephone'] ?? '',
|
||||
'last_rdv_fmt' => !empty($r['last_rdv']) ? (new DateTime($r['last_rdv']))->format('d/m/Y H:i') : '',
|
||||
'last_note' => $r['last_note'] ?? ''
|
||||
];
|
||||
}
|
||||
|
||||
echo json_encode(['ok'=>true,'items'=>$items,'total'=>$total,'limit'=>$limit]);
|
||||
} catch (Throwable $e) {
|
||||
error_log('clients/ajax_list error: '.$e->getMessage());
|
||||
http_response_code(500);
|
||||
echo json_encode(['ok'=>false,'message'=>'Erreur serveur']);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../includes/init_praticien.php';
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
function out($ok,$msg,$extra=[]){ echo json_encode(['ok'=>$ok,'message'=>$msg]+$extra); exit; }
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') out(false,'Méthode invalide');
|
||||
|
||||
$praticien_id = (int)($_SESSION['reference_id'] ?? 0);
|
||||
$csrf = (string)($_POST['csrf'] ?? '');
|
||||
if (!$praticien_id || !hash_equals($_SESSION['cl_csrf'] ?? '', $csrf)) out(false,'CSRF/Unauthorized');
|
||||
|
||||
$dossier_id = (int)($_POST['dossier_id'] ?? 0);
|
||||
if ($dossier_id <= 0) out(false,'Requête invalide');
|
||||
|
||||
// Vérif propriété du dossier
|
||||
$own = $pdo->prepare("SELECT 1 FROM client_dossiers WHERE id=:d AND praticien_id=:p AND archived_at IS NULL");
|
||||
$own->execute([':d'=>$dossier_id, ':p'=>$praticien_id]);
|
||||
if (!$own->fetchColumn()) out(false,'Accès refusé');
|
||||
|
||||
try{
|
||||
// Récupérer chemins des fichiers pour suppression disque après commit
|
||||
$filePaths = [];
|
||||
$sf = $pdo->prepare("SELECT f.path
|
||||
FROM client_suivi_files f
|
||||
JOIN client_suivi s ON s.id=f.entry_id
|
||||
WHERE s.dossier_id=:d");
|
||||
$sf->execute([':d'=>$dossier_id]);
|
||||
while($r = $sf->fetch(PDO::FETCH_ASSOC)){
|
||||
$filePaths[] = (string)$r['path'];
|
||||
}
|
||||
|
||||
// Récup IDs des notes
|
||||
$noteIds = [];
|
||||
$sn = $pdo->prepare("SELECT id FROM client_suivi WHERE dossier_id=:d");
|
||||
$sn->execute([':d'=>$dossier_id]);
|
||||
while($r = $sn->fetch(PDO::FETCH_ASSOC)){
|
||||
$noteIds[] = (int)$r['id'];
|
||||
}
|
||||
|
||||
$pdo->beginTransaction();
|
||||
|
||||
if ($noteIds) {
|
||||
$ph = implode(',', array_fill(0, count($noteIds), '?'));
|
||||
$delF = $pdo->prepare("DELETE FROM client_suivi_files WHERE entry_id IN ($ph)");
|
||||
$delF->execute($noteIds);
|
||||
|
||||
$delN = $pdo->prepare("DELETE FROM client_suivi WHERE id IN ($ph)");
|
||||
$delN->execute($noteIds);
|
||||
}
|
||||
|
||||
$delD = $pdo->prepare("DELETE FROM client_dossiers WHERE id=:d");
|
||||
$delD->execute([':d'=>$dossier_id]);
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
// Suppression physique (best-effort)
|
||||
$root = rtrim((string)$_SERVER['DOCUMENT_ROOT'],'/');
|
||||
foreach ($filePaths as $rel) {
|
||||
$abs = $root.$rel;
|
||||
if (is_file($abs)) @unlink($abs);
|
||||
}
|
||||
|
||||
out(true,'Dossier supprimé');
|
||||
|
||||
}catch(Throwable $e){
|
||||
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||
error_log('ajax_supprimer_dossier: '.$e->getMessage());
|
||||
out(false,'Erreur serveur');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../includes/init_praticien.php';
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
function out($ok,$m){ echo json_encode(['ok'=>$ok,'message'=>$m]); exit; }
|
||||
|
||||
$praticien_id = (int)($_SESSION['reference_id'] ?? 0);
|
||||
$csrf=(string)($_POST['csrf'] ?? '');
|
||||
if (!hash_equals($_SESSION['cl_csrf'] ?? '', $csrf)) out(false,'CSRF');
|
||||
|
||||
$file_id = (int)($_POST['file_id'] ?? 0);
|
||||
if ($file_id<=0) out(false,'Bad file');
|
||||
|
||||
$sql = "SELECT f.id, f.path
|
||||
FROM client_suivi_files f
|
||||
JOIN client_suivi s ON s.id=f.entry_id
|
||||
JOIN client_dossiers d ON d.id=s.dossier_id
|
||||
WHERE f.id=:id AND d.praticien_id=:p AND d.archived_at IS NULL";
|
||||
$st = $pdo->prepare($sql);
|
||||
$st->execute([':id'=>$file_id, ':p'=>$praticien_id]);
|
||||
$row = $st->fetch(PDO::FETCH_ASSOC);
|
||||
if(!$row) out(false,'Not allowed');
|
||||
|
||||
$pdo->beginTransaction();
|
||||
$pdo->prepare("DELETE FROM client_suivi_files WHERE id=:id")->execute([':id'=>$file_id]);
|
||||
$pdo->commit();
|
||||
|
||||
// Supprime le fichier disque (best-effort)
|
||||
$abs = rtrim((string)$_SERVER['DOCUMENT_ROOT'],'/').(string)$row['path'];
|
||||
if (is_file($abs)) @unlink($abs);
|
||||
|
||||
out(true,'OK');
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../includes/init_praticien.php';
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
function out($ok, $msg, $extra = []) {
|
||||
echo json_encode(['ok'=>$ok, 'message'=>$msg] + $extra);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') out(false, 'Méthode invalide');
|
||||
|
||||
$praticien_id = (int)($_SESSION['reference_id'] ?? 0);
|
||||
$csrf = (string)($_POST['csrf'] ?? '');
|
||||
if (!$praticien_id || !hash_equals($_SESSION['cl_csrf'] ?? '', $csrf)) {
|
||||
out(false, 'CSRF/Unauthorized');
|
||||
}
|
||||
|
||||
$note_id = (int)($_POST['note_id'] ?? 0);
|
||||
if ($note_id <= 0) out(false, 'Requête invalide');
|
||||
|
||||
try {
|
||||
// Vérifier que la note appartient bien au praticien
|
||||
$check = $pdo->prepare("
|
||||
SELECT s.id, s.dossier_id
|
||||
FROM client_suivi s
|
||||
JOIN client_dossiers d ON d.id = s.dossier_id
|
||||
WHERE s.id = :id AND d.praticien_id = :p AND d.archived_at IS NULL
|
||||
LIMIT 1
|
||||
");
|
||||
$check->execute([':id'=>$note_id, ':p'=>$praticien_id]);
|
||||
$own = $check->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$own) out(false, 'Accès refusé');
|
||||
|
||||
// Récupérer la liste des fichiers pour suppression disque après commit
|
||||
$files = [];
|
||||
$sf = $pdo->prepare("SELECT id, path FROM client_suivi_files WHERE entry_id = :e");
|
||||
$sf->execute([':e'=>$note_id]);
|
||||
while ($row = $sf->fetch(PDO::FETCH_ASSOC)) {
|
||||
$files[] = (string)$row['path'];
|
||||
}
|
||||
|
||||
$pdo->beginTransaction();
|
||||
|
||||
// Supprimer lignes fichiers BDD puis la note
|
||||
$pdo->prepare("DELETE FROM client_suivi_files WHERE entry_id = :e")->execute([':e'=>$note_id]);
|
||||
$pdo->prepare("DELETE FROM client_suivi WHERE id = :id")->execute([':id'=>$note_id]);
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
// Suppression physique (best-effort) après commit
|
||||
$root = rtrim((string)$_SERVER['DOCUMENT_ROOT'], '/');
|
||||
foreach ($files as $rel) {
|
||||
$abs = $root.$rel;
|
||||
if (is_file($abs)) @unlink($abs);
|
||||
}
|
||||
|
||||
out(true, 'Note supprimée');
|
||||
|
||||
} catch (Throwable $e) {
|
||||
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||
error_log('ajax_supprimer_note: '.$e->getMessage());
|
||||
out(false, 'Erreur serveur');
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../includes/init_praticien.php';
|
||||
|
||||
require_once __DIR__ . '/../../includes/header.php';
|
||||
function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
|
||||
|
||||
$praticien_id = (int)($_SESSION['reference_id'] ?? 0);
|
||||
$dossier_id = (int)($_GET['dossier_id'] ?? 0);
|
||||
if ($dossier_id<=0) { header('Location: index.php'); exit; }
|
||||
|
||||
// Vérif propriétaire
|
||||
$ok = $pdo->prepare("SELECT d.id, d.praticien_id, c.nom, c.prenom
|
||||
FROM client_dossiers d JOIN client c ON c.id=d.client_id
|
||||
WHERE d.id=:d AND d.praticien_id=:p AND d.archived_at IS NULL");
|
||||
$ok->execute([':d'=>$dossier_id, ':p'=>$praticien_id]);
|
||||
$dos = $ok->fetch(PDO::FETCH_ASSOC);
|
||||
if(!$dos){ header('Location: index.php'); exit; }
|
||||
|
||||
// CSRF
|
||||
if (empty($_SESSION['cl_csrf'])) $_SESSION['cl_csrf'] = bin2hex(random_bytes(32));
|
||||
$CSRF = $_SESSION['cl_csrf'];
|
||||
|
||||
?>
|
||||
<div class="main1">
|
||||
<div class="container py-5">
|
||||
<div class="shadow p-4 rounded bg-light">
|
||||
<main class="container my-4">
|
||||
<div class="card p-3 shadow-sm">
|
||||
<h4 class="mb-3"><?= h($translations['cl_ajouter_note_titre'] ?? 'Ajouter une note') ?></h4>
|
||||
|
||||
<form id="note-form" enctype="multipart/form-data" autocomplete="off">
|
||||
<input type="hidden" name="csrf" value="<?= h($CSRF) ?>">
|
||||
<input type="hidden" name="dossier_id" value="<?= (int)$dossier_id ?>">
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-md-4">
|
||||
<label class="form-label"><?= h($translations['cl_date_rdv'] ?? 'Date') ?></label>
|
||||
<input type="datetime-local" name="date_rdv" class="form-control" value="<?= h((new DateTime())->format('Y-m-d\TH:i')) ?>">
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<label class="form-label"><?= h($translations['cl_type_rdv'] ?? 'Type') ?></label>
|
||||
<select class="form-select" name="type_rdv">
|
||||
<option value="online"><?= h($translations['cl_type_online'] ?? 'En ligne') ?></option>
|
||||
<option value="cabinet"><?= h($translations['cl_type_cabinet'] ?? 'Au cabinet') ?></option>
|
||||
<option value="tel"><?= h($translations['cl_type_tel'] ?? 'Téléphone') ?></option>
|
||||
<option value="autre"><?= h($translations['cl_type_autre'] ?? 'Autre') ?></option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<label class="form-label"><?= h($translations['cl_duree'] ?? 'Durée (min)') ?></label>
|
||||
<input type="number" name="duree_minutes" class="form-control" min="1" max="600" value="60">
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label"><?= h($translations['cl_titre'] ?? 'Titre') ?></label>
|
||||
<input type="text" name="titre" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label"><?= h($translations['cl_notes'] ?? 'Notes') ?></label>
|
||||
<textarea name="notes" class="form-control" rows="6" required></textarea>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label"><?= h($translations['cl_prochaine_action'] ?? 'Prochaine action') ?></label>
|
||||
<input type="text" name="prochaine_action" class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label"><?= h($translations['cl_pieces_jointes'] ?? 'Pièces jointes') ?></label>
|
||||
<input type="file" name="files[]" class="form-control" multiple
|
||||
accept=".pdf,.jpg,.jpeg,.png,.doc,.docx,.txt">
|
||||
<div class="form-text">Max 8 Mo par fichier.</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 d-flex justify-content-end gap-2">
|
||||
<a href="fiche.php?dossier_id=<?= (int)$dossier_id ?>" class="btn btn-outline-secondary"><?= h($translations['cl_pagination_prec'] ?? 'Annuler') ?></a>
|
||||
<button class="btn btn-primary" type="submit"><?= h($translations['cl_enregistrer'] ?? 'Enregistrer') ?></button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p><?= $translations['copyr'] ?></p>
|
||||
</div>
|
||||
</footer>
|
||||
<script>
|
||||
document.getElementById('note-form').addEventListener('submit', async (e)=>{
|
||||
e.preventDefault();
|
||||
const fd = new FormData(e.currentTarget);
|
||||
const r = await fetch('ajax_ajouter_note.php', {method:'POST', body:fd, credentials:'same-origin'});
|
||||
const j = await r.json();
|
||||
if(j.ok){
|
||||
window.location = 'fiche.php?dossier_id=' + encodeURIComponent(<?= (int)$dossier_id ?>);
|
||||
} else {
|
||||
alert(j.message || 'Erreur');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../includes/init_praticien.php';
|
||||
|
||||
$praticien_id = (int)($_SESSION['reference_id'] ?? 0);
|
||||
$file_id = (int)($_GET['id'] ?? 0);
|
||||
if ($praticien_id<=0 || $file_id<=0) { http_response_code(400); exit('Bad request'); }
|
||||
|
||||
// Vérifie la propriété via jointure
|
||||
$sql = "SELECT f.path, f.mime, f.size_bytes
|
||||
FROM client_suivi_files f
|
||||
JOIN client_suivi s ON s.id=f.entry_id
|
||||
JOIN client_dossiers d ON d.id=s.dossier_id
|
||||
WHERE f.id=:id AND d.praticien_id=:p AND d.archived_at IS NULL
|
||||
LIMIT 1";
|
||||
$st = $pdo->prepare($sql);
|
||||
$st->execute([':id'=>$file_id, ':p'=>$praticien_id]);
|
||||
$row = $st->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$row) { http_response_code(403); exit('Forbidden'); }
|
||||
|
||||
$rel = (string)$row['path'];
|
||||
$abs = rtrim((string)$_SERVER['DOCUMENT_ROOT'], '/').$rel;
|
||||
if (!is_file($abs) || !is_readable($abs)) { http_response_code(404); exit('Not found'); }
|
||||
|
||||
$mime = $row['mime'] ?: 'application/octet-stream';
|
||||
$dlName = basename($rel);
|
||||
|
||||
header('Content-Type: '.$mime);
|
||||
header('Content-Length: '.filesize($abs));
|
||||
header('Content-Disposition: attachment; filename="'.rawurlencode($dlName).'"');
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
readfile($abs);
|
||||
@@ -0,0 +1,171 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../includes/init_praticien.php';
|
||||
|
||||
require_once __DIR__ . '/../../includes/header.php';
|
||||
function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
|
||||
|
||||
$praticien_id = (int)($_SESSION['reference_id'] ?? 0);
|
||||
$note_id = (int)($_GET['note_id'] ?? 0);
|
||||
if ($note_id<=0) { header('Location: index.php'); exit; }
|
||||
|
||||
// Vérif propriétaire + récupérer dossier_id pour le retour
|
||||
$sql = "SELECT s.*, d.praticien_id, d.id AS dossier_id
|
||||
FROM client_suivi s
|
||||
JOIN client_dossiers d ON d.id=s.dossier_id
|
||||
WHERE s.id=:id AND d.praticien_id=:p AND d.archived_at IS NULL";
|
||||
$st = $pdo->prepare($sql);
|
||||
$st->execute([':id'=>$note_id, ':p'=>$praticien_id]);
|
||||
$note = $st->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$note) { header('Location: index.php'); exit; }
|
||||
|
||||
$stF = $pdo->prepare("SELECT id, path, mime, size_bytes FROM client_suivi_files WHERE entry_id=:e ORDER BY id ASC");
|
||||
$stF->execute([':e'=>$note_id]);
|
||||
$files = $stF->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (empty($_SESSION['cl_csrf'])) $_SESSION['cl_csrf'] = bin2hex(random_bytes(32));
|
||||
$CSRF = $_SESSION['cl_csrf'];
|
||||
|
||||
?>
|
||||
<div class="main1">
|
||||
<div class="container py-5">
|
||||
<div class="shadow p-4 rounded bg-light">
|
||||
<main class="container my-4">
|
||||
<div class="card p-3 shadow-sm">
|
||||
<h4 class="mb-3"><?= h($translations['modifier'] ?? 'Modifier') ?></h4>
|
||||
|
||||
<form id="edit-form" enctype="multipart/form-data">
|
||||
<input type="hidden" name="csrf" value="<?= h($CSRF) ?>">
|
||||
<input type="hidden" name="note_id" value="<?= (int)$note_id ?>">
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-md-4">
|
||||
<label class="form-label"><?= h($translations['cl_date_rdv'] ?? 'Date') ?></label>
|
||||
<input type="datetime-local" name="date_rdv" class="form-control"
|
||||
value="<?= h((new DateTime($note['date_rdv']))->format('Y-m-d\TH:i')) ?>">
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<label class="form-label"><?= h($translations['cl_type_rdv'] ?? 'Type') ?></label>
|
||||
<select class="form-select" name="type_rdv">
|
||||
<?php foreach([''.$translations['cl_type_online'].'',''.$translations['cl_type_cabinet'].'',''.$translations['cl_type_tel'].'',''.$translations['cl_type_autre'].''] as $opt): ?>
|
||||
<option value="<?= $opt ?>" <?= $note['type_rdv']===$opt?'selected':'' ?>><?= h($opt) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<label class="form-label"><?= h($translations['cl_duree'] ?? 'Durée (min)') ?></label>
|
||||
<input type="number" name="duree_minutes" class="form-control" min="1" max="600" value="<?= h((string)($note['duree_minutes'] ?? '')) ?>">
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label"><?= h($translations['cl_titre'] ?? 'Titre') ?></label>
|
||||
<input type="text" name="titre" class="form-control" required value="<?= h($note['titre']) ?>">
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label"><?= h($translations['cl_notes'] ?? 'Notes') ?></label>
|
||||
<textarea name="notes" class="form-control" rows="6" required><?= h($note['notes']) ?></textarea>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label"><?= h($translations['cl_prochaine_action'] ?? 'Prochaine action') ?></label>
|
||||
<input type="text" name="prochaine_action" class="form-control" value="<?= h((string)$note['prochaine_action']) ?>">
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label"><?= h($translations['cl_pieces_jointes'] ?? 'Pièces jointes') ?></label>
|
||||
<?php if($files): ?>
|
||||
<ul class="list-unstyled">
|
||||
<?php foreach($files as $f): $name = basename((string)$f['path']); ?>
|
||||
<li class="d-flex align-items-center gap-2 mb-1">
|
||||
<span class="small"><?= h($name) ?></span>
|
||||
<a class="btn btn-outline-secondary btn--no-shadow" href="download.php?id=<?= (int)$f['id'] ?>">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M.5 9.9a.5.5 0 0 1 .5.5V13a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V10.4a.5.5 0 0 1 1 0V13a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V10.4a.5.5 0 0 1 .5-.5z"/>
|
||||
<path d="M7.646 10.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 9.293V1.5a.5.5 0 0 0-1 0v7.793L5.354 7.146a.5.5 0 1 0-.708.708l3 3z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<button type="button" class="btn btn-outline-danger btn--no-shadow"
|
||||
data-file="<?= (int)$f['id'] ?>">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M5.5 5.5A.5.5 0 0 1 6 5h4a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-1 0V6h-3v6.5a.5.5 0 0 1-1 0v-7z"/>
|
||||
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1 0-2h3l.5-.5h4l.5.5h3a1 1 0 0 1 1 1zM4.118 4l.82 9.823A1 1 0 0 0 5.933 15h4.134a1 1 0 0 0 .995-.823L11.882 4H4.118z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
<input type="file" name="files[]" class="form-control" multiple accept=".pdf,.jpg,.jpeg,.png,.doc,.docx,.txt">
|
||||
<div class="form-text"><?= $translations['8_max']?></div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 d-flex justify-content-end gap-2">
|
||||
<button class="btn btn-primary" type="submit"><?= h($translations['enregistrer'] ?? 'Enregistrer') ?></button>
|
||||
<a href="fiche.php?dossier_id=<?= (int)$note['dossier_id'] ?>" class="btn btn-outline-secondary"><?= h($translations['cl_pagination_prec'] ?? 'Annuler') ?></a>
|
||||
<button type="button" id="btn-delete-note" class="btn btn-outline-danger btn--no-shadow">
|
||||
<?= h($translations['supprimer'] ?? 'Supprimer') ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p><?= $translations['copyr'] ?></p>
|
||||
</div>
|
||||
</footer>
|
||||
<script>
|
||||
document.getElementById('btn-delete-note')?.addEventListener('click', async ()=>{
|
||||
const res = await Swal.fire({
|
||||
title: <?= json_encode($translations['cl_confirmer_suppression']) ?>,
|
||||
text: <?= json_encode($translations['cl_action_irreversible']) ?>,
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: <?= json_encode($translations['btn_supprimer']) ?>,
|
||||
cancelButtonText: <?= json_encode($translations['btn_annuler']) ?>
|
||||
});
|
||||
if (!res.isConfirmed) return;
|
||||
|
||||
const fd = new FormData();
|
||||
fd.append('csrf', '<?= h($CSRF) ?>');
|
||||
fd.append('note_id', '<?= (int)$note['id'] ?>');
|
||||
|
||||
const r = await fetch('ajax_supprimer_note.php', { method:'POST', body:fd, credentials:'same-origin' });
|
||||
const j = await r.json();
|
||||
if (j.ok) {
|
||||
location.href = 'fiche.php?dossier_id=<?= (int)$note['dossier_id'] ?>';
|
||||
} else {
|
||||
Swal.fire('Oops', j.message || 'Erreur', 'error');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
document.getElementById('edit-form').addEventListener('submit', async (e)=>{
|
||||
e.preventDefault();
|
||||
const fd = new FormData(e.currentTarget);
|
||||
const r = await fetch('ajax_editer_note.php',{method:'POST', body:fd, credentials:'same-origin'});
|
||||
const j = await r.json();
|
||||
if(j.ok){
|
||||
location.href = 'fiche.php?dossier_id=<?= (int)$note['dossier_id'] ?>';
|
||||
}else{
|
||||
alert(j.message||'Erreur');
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', async (e)=>{
|
||||
const b = e.target.closest('button[data-file]');
|
||||
if(!b) return;
|
||||
if(!confirm('Supprimer ce fichier ?')) return;
|
||||
const fd = new FormData();
|
||||
fd.append('csrf', '<?= h($CSRF) ?>');
|
||||
fd.append('file_id', b.getAttribute('data-file'));
|
||||
const r = await fetch('ajax_supprimer_fichier.php',{method:'POST', body:fd, credentials:'same-origin'});
|
||||
const j = await r.json();
|
||||
if(j.ok) location.reload(); else alert(j.message||'Erreur');
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,287 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../includes/init_praticien.php';
|
||||
|
||||
|
||||
function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
|
||||
function humanSize($bytes){
|
||||
$u=['B','KB','MB','GB']; $i=0;
|
||||
while($bytes>=1024 && $i<count($u)-1){ $bytes/=1024; $i++; }
|
||||
return number_format($bytes, $i?1:0, ',', ' ').' '.$u[$i];
|
||||
}
|
||||
function fmtDT(?string $s, string $format='d/m/Y H:i'): string {
|
||||
$s = trim((string)$s);
|
||||
if ($s === '' || $s === '0000-00-00' || $s === '0000-00-00 00:00:00') return '—';
|
||||
try { return (new DateTime($s))->format($format); } catch(Throwable $e){ return '—'; }
|
||||
}
|
||||
|
||||
$praticien_id = (int)($_SESSION['reference_id'] ?? 0);
|
||||
$dossier_id = (int)($_GET['dossier_id'] ?? 0);
|
||||
if ($praticien_id<=0 || $dossier_id<=0) { header('Location: index.php'); exit; }
|
||||
|
||||
/* ----- Dossier + Client (vérif propriétaire) ----- */
|
||||
$st = $pdo->prepare("
|
||||
SELECT
|
||||
d.id, d.praticien_id, d.client_id, d.tags,
|
||||
c.nom, c.prenom, c.email, c.telephone, c.adresse, c.ville, c.code_postal, c.date_naissance
|
||||
FROM client_dossiers d
|
||||
JOIN client c ON c.id = d.client_id
|
||||
WHERE d.id = :d AND d.praticien_id = :p AND d.archived_at IS NULL
|
||||
LIMIT 1
|
||||
");
|
||||
$st->execute([':d'=>$dossier_id, ':p'=>$praticien_id]);
|
||||
$dos = $st->fetch(PDO::FETCH_ASSOC);
|
||||
if(!$dos){ header('Location: index.php'); exit; }
|
||||
|
||||
/* ----- Historique RDV ----- */
|
||||
$stR = $pdo->prepare("
|
||||
SELECT date_rdv, duree_minutes, lieu, statut
|
||||
FROM rendez_vous
|
||||
WHERE praticien_id = :p AND client_id = :c AND (statut = 'terminé' OR statut = 'confirmé')
|
||||
ORDER BY date_rdv DESC
|
||||
LIMIT 100
|
||||
");
|
||||
$stR->execute([':p'=>$praticien_id, ':c'=>$dos['client_id']]);
|
||||
$rdvs = $stR->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
/* ----- Notes + fichiers ----- */
|
||||
$stN = $pdo->prepare("
|
||||
SELECT s.*
|
||||
FROM client_suivi s
|
||||
WHERE s.dossier_id = :d
|
||||
ORDER BY s.date_rdv DESC, s.id DESC
|
||||
");
|
||||
$stN->execute([':d'=>$dossier_id]);
|
||||
$notes = $stN->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$filesByEntry = [];
|
||||
if ($notes) {
|
||||
$ids = array_column($notes, 'id');
|
||||
$ph = implode(',', array_fill(0, count($ids), '?'));
|
||||
$stF = $pdo->prepare("
|
||||
SELECT f.id, f.entry_id, f.path, f.mime, f.size_bytes
|
||||
FROM client_suivi_files f
|
||||
WHERE f.entry_id IN ($ph)
|
||||
ORDER BY f.id ASC
|
||||
");
|
||||
$stF->execute($ids);
|
||||
while($f = $stF->fetch(PDO::FETCH_ASSOC)){
|
||||
$filesByEntry[(int)$f['entry_id']][] = $f;
|
||||
}
|
||||
}
|
||||
|
||||
/* ----- CSRF pour actions (si besoin) ----- */
|
||||
if (empty($_SESSION['cl_csrf'])) $_SESSION['cl_csrf'] = bin2hex(random_bytes(32));
|
||||
$CSRF = $_SESSION['cl_csrf']; ?>
|
||||
<form id="csrf-form" class="d-none">
|
||||
<input type="hidden" name="csrf" value="<?= h($CSRF) ?>">
|
||||
</form>
|
||||
<?php
|
||||
/* Initiales avatar */
|
||||
$initiales = mb_strtoupper(
|
||||
mb_substr((string)$dos['prenom'],0,1,'UTF-8').
|
||||
mb_substr((string)$dos['nom'],0,1,'UTF-8'),
|
||||
'UTF-8'
|
||||
);
|
||||
$fullName = trim(($dos['prenom'] ?? '').' '.($dos['nom'] ?? ''));
|
||||
require_once __DIR__ . '/../../includes/header.php';
|
||||
?>
|
||||
<div class="main1">
|
||||
<div class="container py-5">
|
||||
<div class="shadow p-4 rounded bg-light">
|
||||
<main class="container my-4">
|
||||
<div class="row g-3">
|
||||
<!-- ===== COLONNE GAUCHE : Identité client ===== -->
|
||||
<div class="col-12">
|
||||
<div class="card p-3 shadow-sm">
|
||||
<h4 class="mb-3"><?= h($translations['cl_identite'] ?? 'Identité') ?></h4>
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<!--<div class="avatar"><?= h($initiales) ?></div>-->
|
||||
<div>
|
||||
<div class="fw-bold fs-5"><?= h($fullName) ?></div>
|
||||
<div class="text-muted">
|
||||
<?= h($dos['email']??'') ?><?= !empty($dos['telephone']) ? ' - '.h($dos['telephone']) : '' ?>
|
||||
</div>
|
||||
<?php
|
||||
$addr = trim(($dos['adresse']??'').' '.($dos['code_postal']??'').' '.($dos['ville']??''));
|
||||
if ($addr !== ''):
|
||||
?>
|
||||
<div class="text-muted"><?= h($addr) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($dos['date_naissance'])): ?>
|
||||
<div class="text-muted">
|
||||
<?= h($translations['label_date_naissance'] ?? 'Date de naissance') ?> :
|
||||
<?= fmtDT($dos['date_naissance'], 'd/m/Y') ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($dos['tags'])): ?>
|
||||
<div class="mt-2"><span class="badge bg-light text-dark"><?= h($dos['tags']) ?></span></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 d-flex gap-2">
|
||||
<a class="btn btn-outline-secondary" href="ajouter_note.php?dossier_id=<?= (int)$dossier_id ?>">
|
||||
<?= h($translations['cl_btn_ajouter_note'] ?? 'Ajouter une note') ?>
|
||||
</a>
|
||||
<a class="btn btn-outline-secondary" href="index.php">
|
||||
<?= h($translations['cl_pagination_prec'] ?? 'Retour') ?>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== COLONNE DROITE : RDV + Notes ===== -->
|
||||
<div class="col-12">
|
||||
|
||||
<!-- Notes de suivi -->
|
||||
<div class="card p-3 shadow-sm">
|
||||
<h4 class="mb-3"><?= h($translations['cl_notes_suivi'] ?? 'Notes de suivi') ?></h4>
|
||||
<?php if(!$notes): ?>
|
||||
<div class="text-muted"><?= h($translations['cl_aucun_resultat'] ?? 'Aucun résultat.') ?></div>
|
||||
<?php else: ?>
|
||||
<?php foreach($notes as $n):
|
||||
$files = $filesByEntry[(int)$n['id']] ?? [];
|
||||
?>
|
||||
<div class="mb-3 p-3 border rounded-3">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<div class="fw-bold"><?= h($n['titre']) ?></div>
|
||||
<div class="text-muted small">
|
||||
<?= h(fmtDT($n['date_rdv'])) ?>
|
||||
· <?= h($translations['cl_type_rdv'] ?? 'Type') ?>: <?= h($n['type_rdv']) ?>
|
||||
<?php if(!empty($n['duree_minutes'])): ?>
|
||||
· <?= h($translations['cl_duree'] ?? 'Durée') ?>: <?= (int)$n['duree_minutes'] ?> min
|
||||
<?php endif; ?>
|
||||
<?php if(!empty($n['prochaine_action'])): ?>
|
||||
<br><b><?= h($translations['cl_prochaine_action'] ?? 'Prochaine action') ?>: <?= h($n['prochaine_action']) ?></b>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-2"><?= nl2br(h($n['notes'])) ?></div>
|
||||
|
||||
<?php if ($files): ?>
|
||||
<div class="mt-3">
|
||||
<div class="fw-semibold mb-1">📎 <?= h($translations['pieces_jointes'] ?? 'Pièces jointes') ?></div>
|
||||
<ul class="list-unstyled mb-0">
|
||||
<?php foreach($files as $f):
|
||||
$name = basename((string)$f['path']);
|
||||
?>
|
||||
<li class="d-flex align-items-center gap-2 mb-1">
|
||||
<span class="small"><?= h($name) ?></span>
|
||||
<?php if(!empty($f['size_bytes'])): ?>
|
||||
<span class="text-muted small">· <?= h(humanSize((int)$f['size_bytes'])) ?></span>
|
||||
<?php endif; ?>
|
||||
<a href="download.php?id=<?= (int)$f['id'] ?>" class="btn btn-outline-secondary btn-sm" aria-label="Télécharger">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true" focusable="false">
|
||||
<path d="M.5 9.9a.5.5 0 0 1 .5.5V13a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V10.4a.5.5 0 0 1 1 0V13a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V10.4a.5.5 0 0 1 .5-.5z"/>
|
||||
<path d="M7.646 10.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 9.293V1.5a.5.5 0 0 0-1 0v7.793L5.354 7.146a.5.5 0 1 0-.708.708l3 3z"/>
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<br>
|
||||
<a class="btn btn-outline-secondary"
|
||||
href="editer_note.php?note_id=<?= (int)$n['id'] ?>">
|
||||
<?= h($translations['modifier'] ?? 'Modifier') ?>
|
||||
</a>
|
||||
<!-- Ajout de la classe attendue + data-note existant -->
|
||||
<button type="button"
|
||||
class="btn btn-outline-danger btn--no-shadow btn-del-note"
|
||||
data-note="<?= (int)$n['id'] ?>">
|
||||
<?= h($translations['supprimer'] ?? 'Supprimer') ?>
|
||||
</button>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<!-- Historique des rendez-vous -->
|
||||
<div class="card p-3 shadow-sm mb-3">
|
||||
<h4 class="mb-3"><?= h($translations['cl_histo_rdv'] ?? 'Historique des rendez-vous') ?></h4>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?= h($translations['date_label'] ?? 'Date') ?></th>
|
||||
<th><?= h($translations['cl_type_rdv'] ?? 'Type') ?></th>
|
||||
<th><?= h($translations['cl_duree'] ?? 'Durée') ?></th>
|
||||
<th><?= h($translations['cl_col_tags'] ?? 'Statut') ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if(!$rdvs): ?>
|
||||
<tr><td colspan="4" class="text-muted"><?= h($translations['cl_aucun_resultat'] ?? 'Aucun résultat.') ?></td></tr>
|
||||
<?php else: foreach($rdvs as $r): ?>
|
||||
<tr>
|
||||
<td><?= h(fmtDT($r['date_rdv'])) ?></td>
|
||||
<td><?= h($r['lieu']==='online'
|
||||
? ($translations['cl_type_online']??'En ligne')
|
||||
: ($r['lieu']==='offline' ? ($translations['cl_type_cabinet']??'Au cabinet') : ($translations['cl_type_autre']??'Autre'))) ?></td>
|
||||
<td><?= (int)($r['duree_minutes']??0) ?> min</td>
|
||||
<td><?= h($r['statut']) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p><?= $translations['copyr'] ?></p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sweetalert2@11/dist/sweetalert2.min.css">
|
||||
<script src="/js/sweetalert2@11.js"></script>
|
||||
<script>
|
||||
document.addEventListener('click', async (e)=>{
|
||||
// Plus robuste : on accepte soit .btn-del-note soit tout élément portant data-note
|
||||
const btn = e.target.closest('.btn-del-note, [data-note]');
|
||||
if (!btn) return;
|
||||
|
||||
const noteId = btn.getAttribute('data-note') || '';
|
||||
const res = await Swal.fire({
|
||||
title: <?= json_encode($translations['cl_confirmer_suppression']) ?>,
|
||||
text: <?= json_encode($translations['cl_action_irreversible']) ?>,
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: <?= json_encode($translations['btn_supprimer']) ?>,
|
||||
cancelButtonText: <?= json_encode($translations['btn_annuler']) ?>
|
||||
});
|
||||
if (!res.isConfirmed) return;
|
||||
|
||||
const fd = new FormData(document.getElementById('csrf-form'));
|
||||
fd.append('note_id', noteId);
|
||||
|
||||
try{
|
||||
const r = await fetch('ajax_supprimer_note.php', {
|
||||
method:'POST',
|
||||
body: fd,
|
||||
credentials:'same-origin'
|
||||
});
|
||||
const j = await r.json();
|
||||
if (j.ok) {
|
||||
location.reload();
|
||||
} else {
|
||||
Swal.fire('Oops', j.message || 'Erreur', 'error');
|
||||
}
|
||||
}catch(err){
|
||||
Swal.fire('Oops', 'Erreur réseau', 'error');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,278 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../includes/init_praticien.php';
|
||||
|
||||
|
||||
function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
|
||||
|
||||
$praticien_id = (int)($_SESSION['reference_id'] ?? 0);
|
||||
if ($praticien_id <= 0) { header('Location:/'); exit; }
|
||||
|
||||
/* CSRF */
|
||||
if (empty($_SESSION['cl_csrf'])) $_SESSION['cl_csrf'] = bin2hex(random_bytes(32));
|
||||
$CSRF = $_SESSION['cl_csrf'];
|
||||
|
||||
/* === SSR: première page serveur === */
|
||||
$limit = 10; $offset = 0;
|
||||
|
||||
$sqlTotal = "SELECT COUNT(*) FROM client_dossiers d
|
||||
JOIN client c ON c.id=d.client_id
|
||||
WHERE d.praticien_id=:p AND d.archived_at IS NULL";
|
||||
$stT = $pdo->prepare($sqlTotal);
|
||||
$stT->execute([':p'=>$praticien_id]);
|
||||
$total = (int)$stT->fetchColumn();
|
||||
|
||||
/* ➜ ICI on lit prochaine_action et on l’alias en last_note */
|
||||
$sql = "SELECT d.id AS dossier_id, d.tags,
|
||||
c.nom, c.prenom, c.email, c.telephone,
|
||||
(SELECT MAX(rv.date_rdv) FROM rendez_vous rv
|
||||
WHERE rv.client_id=d.client_id AND rv.praticien_id=d.praticien_id
|
||||
AND rv.statut IN ('confirmé','terminé')) AS last_rdv,
|
||||
(SELECT LEFT(s.prochaine_action, 120) FROM client_suivi s
|
||||
WHERE s.dossier_id=d.id
|
||||
ORDER BY s.date_rdv DESC, s.id DESC LIMIT 1) AS last_note
|
||||
FROM client_dossiers d
|
||||
JOIN client c ON c.id=d.client_id
|
||||
WHERE d.praticien_id=:p AND d.archived_at IS NULL
|
||||
ORDER BY (last_rdv IS NULL) ASC, last_rdv DESC, d.updated_at DESC
|
||||
LIMIT :lim OFFSET :off";
|
||||
$st = $pdo->prepare($sql);
|
||||
$st->bindValue(':p',$praticien_id,PDO::PARAM_INT);
|
||||
$st->bindValue(':lim',$limit,PDO::PARAM_INT);
|
||||
$st->bindValue(':off',$offset,PDO::PARAM_INT);
|
||||
$st->execute();
|
||||
$itemsSSR = $st->fetchAll(PDO::FETCH_ASSOC);
|
||||
$pages = max(1, (int)ceil($total/$limit));
|
||||
require_once __DIR__ . '/../../includes/header.php';
|
||||
?>
|
||||
<div class="main1">
|
||||
<div class="container py-5">
|
||||
<div class="shadow p-4 rounded bg-light">
|
||||
<main class="container my-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h2 class="m-0"><?= h($translations['cl_suivi_titre'] ?? 'Suivi clients') ?></h2>
|
||||
<div>
|
||||
<input id="search" type="search" class="form-control" style="min-width:260px"
|
||||
placeholder="<?= h($translations['cl_recherche_placeholder'] ?? 'Rechercher (nom, email, téléphone, tag)…') ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive mc-ready">
|
||||
<table id="table_rdv" class="table table-striped align-middle text-center">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?= h($translations['cl_col_client'] ?? 'Client') ?></th>
|
||||
<th><?= h($translations['cl_col_dernier_rdv'] ?? 'Dernier RDV') ?></th>
|
||||
<th><?= h($translations['cl_prochaine_action'] ?? 'Prochaine action') ?></th>
|
||||
<th class="text-center"><?= h($translations['cl_actions'] ?? 'Actions') ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="list-body">
|
||||
<?php if (!$itemsSSR): ?>
|
||||
<tr><td colspan="4"><?= h($translations['cl_aucun_resultat'] ?? 'Aucun résultat.') ?></td></tr>
|
||||
<?php else: ?>
|
||||
<?php foreach($itemsSSR as $r):
|
||||
$last = !empty($r['last_rdv']) ? (new DateTime($r['last_rdv']))->format('d/m/Y H:i') : '';
|
||||
$name = trim(($r['prenom'] ?? '').' '.($r['nom'] ?? ''));
|
||||
?>
|
||||
<tr>
|
||||
<td data-label="Client">
|
||||
<div class="fw-bold"><?= h($name) ?></div>
|
||||
</td>
|
||||
<td data-label="Dernier RDV"><?= h($last ?: '—') ?></td>
|
||||
<td data-label="<?= h($translations['cl_col_prochaine_action'] ?? 'Prochaine action') ?>"><?= h($r['last_note'] ?? '') ?></td>
|
||||
<td class="text-center" data-label="<?= h($translations['cl_actions'] ?? 'Actions') ?>">
|
||||
<div class="row-actions d-flex justify-content-center gap-2">
|
||||
<a class="btn btn-outline-secondary" href="fiche.php?dossier_id=<?= (int)$r['dossier_id'] ?>"><?= h($translations['cl_btn_infos'] ?? 'Informations') ?></a>
|
||||
<a class="btn btn-outline-secondary" href="ajouter_note.php?dossier_id=<?= (int)$r['dossier_id'] ?>"><?= h($translations['cl_btn_ajouter_note'] ?? 'Ajouter une note') ?></a>
|
||||
<button type="button"
|
||||
class="btn btn-outline-danger"
|
||||
onclick="deleteDossier(<?= (int)$r['dossier_id'] ?>, this)">
|
||||
<?= h($translations['cl_btn_supprimer'] ?? 'Supprimer') ?>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-center gap-2 my-3" id="pager">
|
||||
<?php for($p=1;$p<=$pages;$p++): ?>
|
||||
<button type="button" class="btn btn-sm <?= $p===1?'btn-secondary':'btn-outline-secondary' ?>" data-page="<?= $p ?>"><?= $p ?></button>
|
||||
<?php endfor; ?>
|
||||
</div>
|
||||
|
||||
<form id="csrf-form" class="d-none"><input type="hidden" name="csrf" value="<?= h($CSRF) ?>"></form>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p><?= $translations['copyr'] ?></p>
|
||||
</div>
|
||||
</footer>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sweetalert2@11/dist/sweetalert2.min.css">
|
||||
<script src="/js/sweetalert2@11.js"></script>
|
||||
|
||||
<?php
|
||||
$I18N = [
|
||||
'confirmer_suppression' => $translations['cl_confirmer_suppression'] ?? 'Confirmer la suppression ?',
|
||||
'action_irreversible' => $translations['cl_action_irreversible'] ?? 'Cette action est irréversible.',
|
||||
'supprimer' => $translations['cl_btn_supprimer'] ?? 'Supprimer',
|
||||
'annuler' => $translations['btn_annuler'] ?? 'Annuler',
|
||||
'aucun_resultat' => $translations['cl_aucun_resultat'] ?? 'Aucun résultat.',
|
||||
'col_prochaine_action' => $translations['cl_col_prochaine_action'] ?? 'Prochaine action',
|
||||
];
|
||||
?>
|
||||
<script>
|
||||
// === i18n (sûr) ===
|
||||
const I18N = <?= json_encode(
|
||||
$I18N,
|
||||
JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT
|
||||
) ?>;
|
||||
function T(k, d){ return (Object.prototype.hasOwnProperty.call(I18N, k) ? I18N[k] : d); }
|
||||
|
||||
// === Utils ===
|
||||
function escapeHtml(s){ return (s||'').replace(/[&<>"']/g, c=>({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[c])); }
|
||||
function fmtDate(s){ return s || '—'; }
|
||||
function debounce(fn,ms){ let t; return (...a)=>{ clearTimeout(t); t=setTimeout(()=>fn(...a),ms); }; }
|
||||
|
||||
// === Rendu dynamique ===
|
||||
function render(items,total,limit){
|
||||
const tbody = document.getElementById('list-body');
|
||||
tbody.innerHTML = '';
|
||||
if(!items || !items.length){
|
||||
const tr = document.createElement('tr');
|
||||
const td = document.createElement('td');
|
||||
td.colSpan = 4;
|
||||
td.textContent = T('aucun_resultat');
|
||||
tr.appendChild(td); tbody.appendChild(tr);
|
||||
} else {
|
||||
items.forEach(it=>{
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td data-label="<?= h($translations['cl_col_client'] ?? 'Client') ?>">
|
||||
<div class="fw-bold">${escapeHtml(it.client_name||'')}</div>
|
||||
</td>
|
||||
<td data-label="<?= h($translations['cl_col_dernier_rdv'] ?? 'Dernier RDV') ?>">${escapeHtml(fmtDate(it.last_rdv_fmt))}</td>
|
||||
<td data-label="${escapeHtml(I18N.col_prochaine_action)}">${escapeHtml(it.last_note||'')}</td>
|
||||
<td class="text-center" data-label="<?= h($translations['cl_actions'] ?? 'Actions') ?>">
|
||||
<div class="row-actions d-flex justify-content-center gap-2">
|
||||
<a class="btn btn-outline-secondary" href="fiche.php?dossier_id=${encodeURIComponent(it.dossier_id)}"><?= h($translations['cl_btn_infos'] ?? 'Informations') ?></a>
|
||||
<a class="btn btn-outline-secondary" href="ajouter_note.php?dossier_id=${encodeURIComponent(it.dossier_id)}"><?= h($translations['cl_btn_ajouter_note'] ?? 'Ajouter une note') ?></a>
|
||||
<button type="button"
|
||||
class="btn btn-outline-danger"
|
||||
onclick="deleteDossier(${Number(''+it.dossier_id)}, this)">
|
||||
<?= h($translations['cl_btn_supprimer'] ?? 'Supprimer') ?>
|
||||
</button>
|
||||
</div>
|
||||
</td>`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
// Pager
|
||||
const pager = document.getElementById('pager');
|
||||
const pages = Math.max(1, Math.ceil(total/limit));
|
||||
pager.innerHTML = '';
|
||||
for(let p=1; p<=pages; p++){
|
||||
const b=document.createElement('button');
|
||||
b.type='button';
|
||||
b.className='btn btn-sm '+(p===window._page?'btn-secondary':'btn-outline-secondary');
|
||||
b.textContent=String(p);
|
||||
b.dataset.page = String(p);
|
||||
b.onclick=()=>{ window._page=p; load(); };
|
||||
pager.appendChild(b);
|
||||
}
|
||||
}
|
||||
|
||||
// === Data ===
|
||||
window._page = 1;
|
||||
window._q = '';
|
||||
|
||||
async function load(){
|
||||
try{
|
||||
const url = `ajax_list.php?q=${encodeURIComponent(window._q)}&page=${window._page}&_=${Date.now()}`;
|
||||
const r = await fetch(url, { credentials:'same-origin', cache:'no-store' });
|
||||
const txt = await r.text();
|
||||
let j;
|
||||
try { j = JSON.parse(txt); }
|
||||
catch(parseErr){ console.error('ajax_list.php non-JSON:', txt); throw new Error('Réponse invalide'); }
|
||||
if(!j.ok) throw new Error(j.message||'Erreur');
|
||||
render(j.items||[], j.total||0, j.limit||10);
|
||||
}catch(e){
|
||||
console.error('load() failed:', e);
|
||||
render([], 0, 10);
|
||||
}
|
||||
}
|
||||
|
||||
// === Recherche ===
|
||||
document.getElementById('search').addEventListener('input', debounce((e)=>{ window._q=e.target.value||''; window._page=1; load(); }, 300));
|
||||
|
||||
// === Suppression : fonction globale appelée par onclick ===
|
||||
window.deleteDossier = async function(dossierId, btnEl){
|
||||
try{
|
||||
const confirmWithSwal = async ()=>{
|
||||
if (typeof Swal !== 'undefined' && Swal.fire) {
|
||||
const res = await Swal.fire({
|
||||
title: T('confirmer_suppression'),
|
||||
text: T('action_irreversible'),
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: T('supprimer'),
|
||||
cancelButtonText: T('annuler')
|
||||
});
|
||||
return res.isConfirmed;
|
||||
}
|
||||
return window.confirm('Supprimer ce dossier ?');
|
||||
};
|
||||
if (!(await confirmWithSwal())) return;
|
||||
|
||||
btnEl?.setAttribute('disabled','disabled');
|
||||
btnEl?.classList.add('disabled');
|
||||
|
||||
const fd = new FormData(document.getElementById('csrf-form'));
|
||||
fd.append('dossier_id', String(dossierId));
|
||||
|
||||
const r = await fetch('ajax_supprimer_dossier.php', {
|
||||
method:'POST', body:fd, credentials:'same-origin', cache:'no-store'
|
||||
});
|
||||
|
||||
let j;
|
||||
try { j = await r.json(); }
|
||||
catch(_){ const txt = await r.text(); console.error('Réponse non-JSON de ajax_supprimer_dossier.php:', txt); alert('Erreur serveur'); return; }
|
||||
|
||||
if (j.ok) {
|
||||
const tr = btnEl?.closest('tr'); if (tr) tr.remove();
|
||||
const rowsLeft = document.querySelectorAll('#list-body > tr').length;
|
||||
if (rowsLeft === 0 && window._page > 1) window._page--;
|
||||
await load();
|
||||
if (typeof Swal !== 'undefined' && Swal.fire) {
|
||||
Swal.fire({ icon:'success', title:'OK', text:'Dossier supprimé', timer:1200, showConfirmButton:false });
|
||||
}
|
||||
} else {
|
||||
alert(j.message || 'Erreur');
|
||||
}
|
||||
}catch(err){
|
||||
console.error(err);
|
||||
alert('Erreur réseau');
|
||||
}finally{
|
||||
btnEl?.removeAttribute('disabled');
|
||||
btnEl?.classList.remove('disabled');
|
||||
}
|
||||
};
|
||||
|
||||
// === Pager SSR (premier rendu) ===
|
||||
document.getElementById('pager').addEventListener('click', (e)=>{
|
||||
const b = e.target.closest('button[data-page]');
|
||||
if(!b) return;
|
||||
window._page = parseInt(b.dataset.page || '1', 10) || 1;
|
||||
load();
|
||||
});
|
||||
|
||||
// === Initial load ===
|
||||
load();
|
||||
</script>
|
||||
@@ -0,0 +1,160 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../includes/init_praticien.php';
|
||||
|
||||
$success = $erreur = '';
|
||||
$id = $_SESSION['compte_id'];
|
||||
|
||||
// Récupérer e-mail actuel
|
||||
$stmt = $pdo->prepare("SELECT email FROM compte WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$compte = $stmt->fetch();
|
||||
|
||||
if (!$compte) {
|
||||
die($translations['compte_introuvable']);
|
||||
}
|
||||
|
||||
$email_actuel = $compte['email'];
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$nouvel_email = trim($_POST['email'] ?? '');
|
||||
$mdp1 = $_POST['mot_de_passe'] ?? '';
|
||||
$mdp2 = $_POST['confirm_mot_de_passe'] ?? '';
|
||||
|
||||
if (!filter_var($nouvel_email, FILTER_VALIDATE_EMAIL)) {
|
||||
$erreur = $translations['erreur_email_non_valide'];
|
||||
} elseif ($mdp1 && ($mdp1 !== $mdp2 || strlen($mdp1) < 8)) {
|
||||
$erreur = $translations['erreur_mdp_invalides'];
|
||||
} else {
|
||||
// Vérifier si l'email est déjà pris (sauf si c’est le sien)
|
||||
$stmt = $pdo->prepare("SELECT id FROM compte WHERE email = ? AND id != ?");
|
||||
$stmt->execute([$nouvel_email, $id]);
|
||||
if ($stmt->fetch()) {
|
||||
$erreur = $translations['err_email_deja_utilise'];
|
||||
} else {
|
||||
// Mise à jour
|
||||
if ($mdp1) {
|
||||
$hash = password_hash($mdp1, PASSWORD_DEFAULT);
|
||||
$stmt = $pdo->prepare("UPDATE compte SET email = ?, mot_de_passe = ? WHERE id = ?");
|
||||
$stmt->execute([$nouvel_email, $hash, $id]);
|
||||
} else {
|
||||
$stmt = $pdo->prepare("UPDATE compte SET email = ? WHERE id = ?");
|
||||
$stmt->execute([$nouvel_email, $id]);
|
||||
}
|
||||
$success = $translations['success_compte_mis_a_jour'];
|
||||
// Optionnel : mettre à jour $email_actuel pour refléter l'écran
|
||||
$email_actuel = $nouvel_email;
|
||||
}
|
||||
}
|
||||
}
|
||||
require_once '../includes/header.php';
|
||||
?>
|
||||
|
||||
<div class="main1">
|
||||
<div class="container py-5">
|
||||
<form method="POST" class="shadow p-4 rounded bg-light">
|
||||
<h2 class="text-center mb-4"><?= htmlspecialchars($translations['h2_infos_connexion']) ?></h2>
|
||||
|
||||
<?php if (!empty($erreur)): ?>
|
||||
<div class="alert alert-danger"><?= htmlspecialchars($erreur) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($success)): ?>
|
||||
<div class="alert alert-success"><?= htmlspecialchars($success) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= htmlspecialchars($translations['label_email']) ?></label>
|
||||
<input type="email" name="email" class="form-control" value="<?= htmlspecialchars($email_actuel ?? '') ?>" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= htmlspecialchars($translations['label_nouveau_mdp_opt']) ?></label>
|
||||
<div class="field-wrapper">
|
||||
<input type="password" name="mot_de_passe" id="mot_de_passe" class="form-control"
|
||||
placeholder="<?= htmlspecialchars($translations['placeholder_nouveau_mdp']) ?>">
|
||||
<span class="toggle-password" onclick="toggle('mot_de_passe')">👁️</span>
|
||||
</div>
|
||||
<div class="strength-bar mt-2">
|
||||
<div class="strength-fill"></div>
|
||||
<div class="strength-fill"></div>
|
||||
<div class="strength-fill"></div>
|
||||
</div>
|
||||
<div id="niveau-securite" class="mb-2 text-muted">
|
||||
<?= htmlspecialchars($translations['securite_label']) ?> : <?= htmlspecialchars($translations['securite_vide']) ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= htmlspecialchars($translations['label_confirmer_mdp']) ?></label>
|
||||
<div class="field-wrapper">
|
||||
<input type="password" name="confirm_mot_de_passe" id="confirm_mot_de_passe" class="form-control"
|
||||
placeholder="<?= htmlspecialchars($translations['placeholder_confirmer_mdp']) ?>">
|
||||
<span class="toggle-password" onclick="toggle('confirm_mot_de_passe')">👁️</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 text-center">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<?= htmlspecialchars($translations['btn_mettre_a_jour']) ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<script>
|
||||
function toggle(id) {
|
||||
const el = document.getElementById(id);
|
||||
el.type = (el.type === 'password') ? 'text' : 'password';
|
||||
}
|
||||
|
||||
document.getElementById('mot_de_passe').addEventListener('input', function () {
|
||||
const val = this.value;
|
||||
const segments = document.querySelectorAll(".strength-fill");
|
||||
const niveauText = document.getElementById("niveau-securite");
|
||||
|
||||
segments.forEach(s => s.style.backgroundColor = '#ccc');
|
||||
|
||||
// base label
|
||||
const label = <?= json_encode($translations['securite_label'], JSON_UNESCAPED_UNICODE) ?>;
|
||||
const faible = <?= json_encode($translations['securite_faible'], JSON_UNESCAPED_UNICODE) ?>;
|
||||
const moyen = <?= json_encode($translations['securite_moyen'], JSON_UNESCAPED_UNICODE) ?>;
|
||||
const fort = <?= json_encode($translations['securite_fort'], JSON_UNESCAPED_UNICODE) ?>;
|
||||
const vide = <?= json_encode($translations['securite_vide'], JSON_UNESCAPED_UNICODE) ?>;
|
||||
|
||||
niveauText.textContent = `${label} : ${vide}`;
|
||||
niveauText.classList.remove("text-danger", "text-warning", "text-success");
|
||||
niveauText.classList.add("text-muted");
|
||||
|
||||
const longOk = val.length >= 8;
|
||||
const lettre = /[A-Za-z]/.test(val);
|
||||
const chiffre = /\d/.test(val);
|
||||
const maj = /[A-Z]/.test(val);
|
||||
const special = /[^A-Za-z0-9]/.test(val);
|
||||
|
||||
let force = 0;
|
||||
if (longOk && lettre && chiffre) force++;
|
||||
if (maj) force++;
|
||||
if (special) force++;
|
||||
|
||||
if (force === 1) {
|
||||
segments[0].style.backgroundColor = 'red';
|
||||
niveauText.textContent = `${label} : ${faible}`;
|
||||
niveauText.classList.replace("text-muted", "text-danger");
|
||||
} else if (force === 2) {
|
||||
segments[0].style.backgroundColor = 'orange';
|
||||
segments[1].style.backgroundColor = 'orange';
|
||||
niveauText.textContent = `${label} : ${moyen}`;
|
||||
niveauText.classList.replace("text-muted", "text-warning");
|
||||
} else if (force >= 3) {
|
||||
segments.forEach(s => s.style.backgroundColor = 'green');
|
||||
niveauText.textContent = `${label} : ${fort}`;
|
||||
niveauText.classList.replace("text-muted", "text-success");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p><?= $translations['copyr'] ?></p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,283 @@
|
||||
<?php
|
||||
// /public/praticien/disponibilites.php
|
||||
require_once __DIR__ . '/../includes/init_praticien.php';
|
||||
|
||||
$praticien_id = (int) ($_SESSION['reference_id'] ?? 0);
|
||||
if ($praticien_id <= 0) {http_response_code(403);exit('Forbidden');}
|
||||
|
||||
// Durée RDV praticien (garde-fou strict)
|
||||
$stmt = $pdo->prepare("SELECT duree_rdv FROM praticien WHERE id = ?");
|
||||
$stmt->execute([$praticien_id]);
|
||||
$duree_minutes = (int) ($stmt->fetchColumn() ?: 0);
|
||||
if ($duree_minutes <= 0) {
|
||||
$duree_minutes = 30;
|
||||
}
|
||||
// fallback sécurité
|
||||
// ---- Ajouter un créneau (POST) ----
|
||||
$erreur = null;
|
||||
|
||||
$toMin = function (string $hhmm): ?int {
|
||||
// input type="time" renvoie souvent HH:MM
|
||||
if (! preg_match('/^\d{2}:\d{2}$/', $hhmm)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
[$h, $m] = array_map('intval', explode(':', $hhmm));
|
||||
if ($h < 0 || $h > 23 || $m < 0 || $m > 59) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $h * 60 + $m;
|
||||
};
|
||||
|
||||
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']);
|
||||
|
||||
$md = $toMin($debut);
|
||||
$mf = $toMin($fin);
|
||||
|
||||
if ($md === null || $mf === null) {
|
||||
$erreur = $translations['erreur_format_heure'] ?? "Format d'heure invalide.";
|
||||
} elseif ($md >= $mf) {
|
||||
$erreur = $translations['erreur_debut_avant_fin'] ?? "Heure de début après l’heure de fin.";
|
||||
} else {
|
||||
$window = $mf - $md; // minutes
|
||||
|
||||
if ($window < $duree_minutes) {
|
||||
$erreur = sprintf(
|
||||
$translations['erreur_plage_trop_courte'] ?? "Créneau trop court (%d min). Minimum : %d min.",
|
||||
$window,
|
||||
$duree_minutes
|
||||
);
|
||||
} elseif (($window % $duree_minutes) !== 0) {
|
||||
$erreur = sprintf(
|
||||
$translations['erreur_plage_incompatible_duree'] ?? "Créneau incompatible. La durée doit être un multiple de %d min.",
|
||||
$duree_minutes
|
||||
);
|
||||
} else {
|
||||
$stmt = $pdo->prepare(
|
||||
"INSERT INTO disponibilite (praticien_id, jour_semaine, heure_debut, heure_fin)
|
||||
VALUES (?, ?, ?, ?)"
|
||||
);
|
||||
$stmt->execute([$praticien_id, $jour, $debut, $fin]);
|
||||
|
||||
header('Location: /public/praticien/disponibilites.php?ok=added', true, 303);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Supprimer un créneau (GET) ----
|
||||
// (idéalement: POST + CSRF, on garde GET pour aller vite)
|
||||
if (isset($_GET['supprimer'])) {
|
||||
$id = (int) $_GET['supprimer'];
|
||||
$stmt = $pdo->prepare("DELETE FROM disponibilite WHERE id = ? AND praticien_id = ?");
|
||||
$stmt->execute([$id, $praticien_id]);
|
||||
header('Location: /public/praticien/disponibilites.php?ok=deleted', true, 303);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ---- 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',
|
||||
];
|
||||
// 1️⃣ Titre de la page
|
||||
$PAGE_TITLE = $translations['titre_disponibilites'] ?? 'Disponibilités';
|
||||
|
||||
// 2️⃣ Ce qui doit aller dans <head> EN PLUS du header global
|
||||
$HEAD_EXTRA = <<<HTML
|
||||
<script src="/js/sweetalert2@11.js"></script>
|
||||
HTML;
|
||||
require_once __DIR__ . '/../includes/header.php';
|
||||
?>
|
||||
|
||||
|
||||
<div class="main1">
|
||||
<div class="container py-5">
|
||||
<div class="shadow p-4 rounded bg-light">
|
||||
<h2 class="mb-4 text-center"><?php echo htmlspecialchars($translations['h2_disponibilites'] ?? 'Mes disponibilités') ?></h2>
|
||||
|
||||
<!-- Plus d'alertes Bootstrap ici : on affiche via Swal selon ?ok=... et $erreur -->
|
||||
<div class="text-end">
|
||||
<a href="/public/praticien/disponibilites_date.php" class="btn btn-outline-secondary mb-4">
|
||||
<?php echo 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><?php echo htmlspecialchars($translations['table_jour'] ?? 'Jour') ?></th>
|
||||
<th><?php echo htmlspecialchars($translations['table_debut'] ?? 'Début') ?></th>
|
||||
<th><?php echo htmlspecialchars($translations['table_fin'] ?? 'Fin') ?></th>
|
||||
<th><?php echo htmlspecialchars($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 htmlspecialchars($key ? ($translations[$key] ?? ucfirst($d['jour_semaine'])) : ucfirst($d['jour_semaine']));
|
||||
?>
|
||||
</td>
|
||||
<td><?php echo htmlspecialchars(substr((string) $d['heure_debut'], 0, 5)) ?></td>
|
||||
<td><?php echo htmlspecialchars(substr((string) $d['heure_fin'], 0, 5)) ?></td>
|
||||
<td>
|
||||
<button type="button"
|
||||
class="btn btn-sm btn-danger js-del"
|
||||
data-id="<?php echo (int) $d['id'] ?>">
|
||||
<?php echo htmlspecialchars($translations['btn_supprimer'] ?? 'Supprimer') ?>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach;else: ?>
|
||||
<tr><td colspan="4"><?php echo htmlspecialchars($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"><?php echo htmlspecialchars($translations['card_ajouter_creneau'] ?? 'Ajouter un créneau') ?></h4>
|
||||
<form method="POST">
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="jour"><?php echo htmlspecialchars($translations['label_jour'] ?? 'Jour') ?></label>
|
||||
<select name="jour" id="jour" class="form-select" required>
|
||||
<option value=""><?php echo htmlspecialchars($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="<?php echo htmlspecialchars($val) ?>"><?php echo htmlspecialchars($label) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="heure_debut"><?php echo htmlspecialchars($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"><?php echo htmlspecialchars($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">
|
||||
<?php echo htmlspecialchars($translations['btn_ajouter'] ?? 'Ajouter') ?>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// --- SweetAlert2: messages succès / erreur depuis PHP ---
|
||||
document.addEventListener('DOMContentLoaded', function(){
|
||||
const okParam = '<?php echo isset($_GET['ok']) ? addslashes((string) $_GET['ok']) : '' ?>';
|
||||
const erreurMsg = '<?php echo $erreur ? addslashes((string) $erreur) : '' ?>';
|
||||
|
||||
if (okParam === 'added') {
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: '<?php echo addslashes($translations["msg_creneau_ajoute"] ?? "Créneau ajouté.") ?>',
|
||||
timer: 1800,
|
||||
showConfirmButton: false
|
||||
});
|
||||
} else if (okParam === 'deleted') {
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: '<?php echo addslashes($translations["msg_creneau_supprime"] ?? "Créneau supprimé.") ?>',
|
||||
timer: 1800,
|
||||
showConfirmButton: false
|
||||
});
|
||||
}
|
||||
|
||||
if (erreurMsg) {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: '<?php echo addslashes($translations["erreur_titre"] ?? "Erreur") ?>',
|
||||
html: erreurMsg,
|
||||
showCancelButton: true,
|
||||
confirmButtonText: '<?php echo addslashes($translations["btn_modifier_profil"] ?? "Modifier mon profil") ?>',
|
||||
cancelButtonText: 'OK'
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
window.location.href = '/public/praticien/modifier.php';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// --- 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: '<?php echo addslashes($translations["confirm_title"] ?? "Confirmation") ?>',
|
||||
text: '<?php echo addslashes($translations["confirm_supprimer_creneau"] ?? "Supprimer ce créneau ?") ?>',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: '<?php echo addslashes($translations["btn_da_udalit"] ?? "Oui") ?>',
|
||||
cancelButtonText: '<?php echo addslashes($translations["btn_annuler"] ?? "Annuler") ?>',
|
||||
reverseButtons: true
|
||||
});
|
||||
|
||||
if (result.isConfirmed) {
|
||||
// Suppression côté serveur (GET) — garde la mécanique existante
|
||||
window.location.href = '?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><?php echo $translations['copyr'] ?? '© ' . date('Y') ?></p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,249 @@
|
||||
<?php
|
||||
// /public/praticien/disponibilites_date.php
|
||||
require_once __DIR__ . '/../includes/init_praticien.php';
|
||||
$praticien_id = (int) ($_SESSION['reference_id'] ?? 0);
|
||||
if ($praticien_id <= 0) {http_response_code(403);exit('Forbidden');}
|
||||
|
||||
// Durée RDV praticien (garde-fou strict)
|
||||
$stmt = $pdo->prepare("SELECT duree_rdv FROM praticien WHERE id = ?");
|
||||
$stmt->execute([$praticien_id]);
|
||||
$duree_minutes = (int) ($stmt->fetchColumn() ?: 0);
|
||||
if ($duree_minutes <= 0) {
|
||||
$duree_minutes = 30;
|
||||
}
|
||||
// ---- Ajouter une dispo ponctuelle (POST) ----
|
||||
$erreur = null;
|
||||
|
||||
$toMin = function (string $hhmm): ?int {
|
||||
if (! preg_match('/^\d{2}:\d{2}$/', $hhmm)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
[$h, $m] = array_map('intval', explode(':', $hhmm));
|
||||
if ($h < 0 || $h > 23 || $m < 0 || $m > 59) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $h * 60 + $m;
|
||||
};
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['date_dispo'], $_POST['heure_debut'], $_POST['heure_fin'])) {
|
||||
$date_dispo = trim((string) $_POST['date_dispo']); // YYYY-MM-DD
|
||||
$debut = trim((string) $_POST['heure_debut']); // HH:MM
|
||||
$fin = trim((string) $_POST['heure_fin']); // HH:MM
|
||||
|
||||
// Validation date simple
|
||||
$dt = DateTimeImmutable::createFromFormat('Y-m-d', $date_dispo);
|
||||
if (! $dt || $dt->format('Y-m-d') !== $date_dispo) {
|
||||
$erreur = $translations['erreur_format_date'] ?? "Format de date invalide.";
|
||||
} else {
|
||||
$md = $toMin($debut);
|
||||
$mf = $toMin($fin);
|
||||
|
||||
if ($md === null || $mf === null) {
|
||||
$erreur = $translations['erreur_format_heure'] ?? "Format d'heure invalide.";
|
||||
} elseif ($md >= $mf) {
|
||||
$erreur = $translations['erreur_debut_avant_fin'] ?? "Heure de début après l’heure de fin.";
|
||||
} else {
|
||||
$window = $mf - $md;
|
||||
|
||||
if ($window < $duree_minutes) {
|
||||
$erreur = sprintf(
|
||||
$translations['erreur_plage_trop_courte'] ?? "Créneau trop court (%d min). Minimum : %d min.",
|
||||
$window,
|
||||
$duree_minutes
|
||||
);
|
||||
} elseif (($window % $duree_minutes) !== 0) {
|
||||
$erreur = sprintf(
|
||||
$translations['erreur_plage_incompatible_duree'] ?? "Créneau incompatible. La durée doit être un multiple de %d min.",
|
||||
$duree_minutes
|
||||
);
|
||||
} else {
|
||||
$stmt = $pdo->prepare(
|
||||
"INSERT INTO disponibilite_date (praticien_id, date_dispo, heure_debut, heure_fin)
|
||||
VALUES (?, ?, ?, ?)"
|
||||
);
|
||||
$stmt->execute([$praticien_id, $date_dispo, $debut, $fin]);
|
||||
|
||||
header('Location: /public/praticien/disponibilites_date.php?ok=added', true, 303);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Supprimer une plage datée (GET) ----
|
||||
if (isset($_GET['supprimer'])) {
|
||||
$id = (int) $_GET['supprimer'];
|
||||
$stmt = $pdo->prepare("DELETE FROM disponibilite_date WHERE id = ? AND praticien_id = ?");
|
||||
$stmt->execute([$id, $praticien_id]);
|
||||
header('Location: /public/praticien/disponibilites_date.php?ok=deleted', true, 303);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ---- Récupérer les plages datées ----
|
||||
$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);
|
||||
// 1️⃣ Titre de la page
|
||||
$PAGE_TITLE = $translations['titre_disponibilites_date'] ?? 'Disponibilités (dates)';
|
||||
|
||||
// 2️⃣ Ce qui doit aller dans <head> EN PLUS du header global
|
||||
$HEAD_EXTRA = <<<HTML
|
||||
<script src="/js/sweetalert2@11.js"></script>
|
||||
HTML;
|
||||
require_once __DIR__ . '/../includes/header.php';
|
||||
?>
|
||||
|
||||
|
||||
<div class="main1">
|
||||
<div class="container py-5">
|
||||
<div class="shadow p-4 rounded bg-light">
|
||||
<h2 class="mb-4 text-center"><?php echo htmlspecialchars($translations['h2_disponibilites_date'] ?? 'Mes disponibilités ponctuelles') ?></h2>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<a class="btn btn-outline-secondary"
|
||||
href="/public/praticien/disponibilites.php">
|
||||
<?php echo htmlspecialchars($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_rdv">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th><?php echo htmlspecialchars($translations['cl_date_rdv'] ?? 'Date') ?></th>
|
||||
<th><?php echo htmlspecialchars($translations['table_debut'] ?? 'Début') ?></th>
|
||||
<th><?php echo htmlspecialchars($translations['table_fin'] ?? 'Fin') ?></th>
|
||||
<th><?php echo htmlspecialchars($translations['table_action'] ?? 'Action') ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if ($dates): foreach ($dates as $d): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<?php echo htmlspecialchars(
|
||||
date('d/m/Y', strtotime($d['date_dispo']))
|
||||
) ?>
|
||||
</td>
|
||||
<td><?php echo htmlspecialchars(substr((string) $d['heure_debut'], 0, 5)) ?></td>
|
||||
<td><?php echo htmlspecialchars(substr((string) $d['heure_fin'], 0, 5)) ?></td>
|
||||
<td>
|
||||
<button type="button"
|
||||
class="btn btn-sm btn-danger js-del"
|
||||
data-id="<?php echo (int) $d['id'] ?>">
|
||||
<?php echo htmlspecialchars($translations['btn_supprimer'] ?? 'Supprimer') ?>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach;else: ?>
|
||||
<tr><td colspan="4"><?php echo htmlspecialchars($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"><?php echo htmlspecialchars($translations['card_ajouter_creneau_date'] ?? 'Ajouter une disponibilité ponctuelle') ?></h4>
|
||||
|
||||
<form method="POST">
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="date_dispo"><?php echo htmlspecialchars($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"><?php echo htmlspecialchars($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"><?php echo htmlspecialchars($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">
|
||||
<?php echo htmlspecialchars($translations['btn_ajouter'] ?? 'Ajouter') ?>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function(){
|
||||
const okParam = '<?php echo isset($_GET['ok']) ? addslashes((string) $_GET['ok']) : '' ?>';
|
||||
const erreurMsg = '<?php echo $erreur ? addslashes((string) $erreur) : '' ?>';
|
||||
|
||||
if (okParam === 'added') {
|
||||
Swal.fire({ icon:'success', title:'<?php echo addslashes($translations["msg_creneau_ajoute"] ?? "Créneau ajouté.") ?>', timer:1800, showConfirmButton:false });
|
||||
} else if (okParam === 'deleted') {
|
||||
Swal.fire({ icon:'success', title:'<?php echo addslashes($translations["msg_creneau_supprime"] ?? "Créneau supprimé.") ?>', timer:1800, showConfirmButton:false });
|
||||
}
|
||||
|
||||
if (erreurMsg) {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: '<?php echo addslashes($translations["erreur_titre"] ?? "Erreur") ?>',
|
||||
html: erreurMsg,
|
||||
showCancelButton: true,
|
||||
confirmButtonText: '<?php echo addslashes($translations["btn_modifier_profil"] ?? "Modifier mon profil") ?>',
|
||||
cancelButtonText: 'OK'
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
window.location.href = '/public/praticien/modifier.php';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
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: '<?php echo addslashes($translations["confirm_title"] ?? "Confirmation") ?>',
|
||||
text: '<?php echo addslashes($translations["confirm_supprimer_creneau"] ?? "Supprimer ce créneau ?") ?>',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: '<?php echo addslashes($translations["btn_da_udalit"] ?? "Oui") ?>',
|
||||
cancelButtonText: '<?php echo addslashes($translations["btn_annuler"] ?? "Annuler") ?>',
|
||||
reverseButtons: true
|
||||
});
|
||||
|
||||
if (result.isConfirmed) {
|
||||
window.location.href = '?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><?php echo $translations['copyr'] ?? '© ' . date('Y') ?></p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../includes/init_praticien.php';
|
||||
|
||||
|
||||
$praticien_id = $_POST['praticien_id'] ?? 0;
|
||||
if ($praticien_id != $_SESSION['reference_id']) {
|
||||
http_response_code(403);
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT rv.id, rv.date_rdv, rv.duree_minutes, rv.statut,
|
||||
c.nom, c.prenom
|
||||
FROM rendez_vous rv
|
||||
JOIN client c ON rv.client_id = c.id
|
||||
WHERE rv.praticien_id = ?
|
||||
");
|
||||
$stmt->execute([$praticien_id]);
|
||||
$rdvs = $stmt->fetchAll();
|
||||
|
||||
$events = [];
|
||||
|
||||
$couleurs = [
|
||||
'prévu' => '#f39c12', // orange
|
||||
'confirmé' => '#27ae60', // vert
|
||||
'annulé' => '#c0392b', // rouge
|
||||
'terminé' => '#7f8c8d' // gris
|
||||
];
|
||||
|
||||
foreach ($rdvs as $r) {
|
||||
$start = new DateTime($r['date_rdv']);
|
||||
$end = (clone $start)->modify("+{$r['duree_minutes']} minutes");
|
||||
$statut = $r['statut'];
|
||||
|
||||
$events[] = [
|
||||
'id' => $r['id'],
|
||||
'title' => $r['prenom'] . ' ' . $r['nom'],
|
||||
'start' => $start->format(DateTime::ATOM),
|
||||
'end' => $end->format(DateTime::ATOM),
|
||||
'color' => $couleurs[$statut] ?? '#999',
|
||||
'statut'=> ucfirst($statut)
|
||||
];
|
||||
}
|
||||
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($events);
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../includes/init_praticien.php';
|
||||
|
||||
|
||||
$praticien_id = $_SESSION['reference_id'];
|
||||
|
||||
$stmt = $pdo->prepare("SELECT * FROM paiement WHERE praticien_id = ? ORDER BY date_paiement DESC");
|
||||
$stmt->execute([$praticien_id]);
|
||||
$paiements = $stmt->fetchAll();
|
||||
|
||||
// Mappings d'affichage
|
||||
$methodeMap = [
|
||||
'cb' => $translations['methode_cb'] ?? 'CB',
|
||||
'virement' => $translations['methode_virement'] ?? 'Virement',
|
||||
'paypal' => $translations['methode_paypal'] ?? 'PayPal',
|
||||
'stripe' => $translations['methode_stripe'] ?? 'Stripe',
|
||||
];
|
||||
|
||||
$statutMap = [
|
||||
'paye' => $translations['statut_paye'] ?? 'Payé',
|
||||
'payé' => $translations['statut_paye'] ?? 'Payé',
|
||||
'succeeded' => $translations['statut_paye'] ?? 'Payé',
|
||||
'pending' => $translations['statut_en_attente'] ?? 'En attente',
|
||||
'en_attente' => $translations['statut_en_attente'] ?? 'En attente',
|
||||
'failed' => $translations['statut_echoue'] ?? 'Échoué',
|
||||
'echoue' => $translations['statut_echoue'] ?? 'Échoué',
|
||||
'échoué' => $translations['statut_echoue'] ?? 'Échoué',
|
||||
'refunded' => $translations['statut_rembourse'] ?? 'Remboursé',
|
||||
'rembourse' => $translations['statut_rembourse'] ?? 'Remboursé',
|
||||
'remboursé' => $translations['statut_rembourse'] ?? 'Remboursé',
|
||||
];
|
||||
|
||||
// helper badge
|
||||
function badgeClassForStatus(string $raw): string {
|
||||
$s = mb_strtolower($raw, 'UTF-8');
|
||||
if (in_array($s, ['paye','payé','succeeded'], true)) return 'bg-success';
|
||||
if (in_array($s, ['pending','en_attente'], true)) return 'bg-warning text-dark';
|
||||
if (in_array($s, ['failed','echoue','échoué'], true)) return 'bg-danger';
|
||||
if (in_array($s, ['refunded','rembourse','remboursé'], true)) return 'bg-secondary';
|
||||
return 'bg-secondary';
|
||||
}
|
||||
require_once '../includes/header.php';
|
||||
?>
|
||||
<div class="main1">
|
||||
<div class="container py-5">
|
||||
<div class="shadow p-4 rounded bg-light">
|
||||
<h3 class="mb-4 text-center"><?= htmlspecialchars($translations['h3_mes_factures']) ?></h3>
|
||||
|
||||
<?php if (empty($paiements)): ?>
|
||||
<div class="alert alert-info">
|
||||
<?= htmlspecialchars($translations['aucune_facture']) ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-hover align-middle mobile-cards" id="table_rdv">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th scope="col"><?= htmlspecialchars($translations['th_numero']) ?></th>
|
||||
<th scope="col"><?= htmlspecialchars($translations['th_date']) ?></th>
|
||||
<th scope="col"><?= htmlspecialchars($translations['th_montant']) ?></th>
|
||||
<th scope="col"><?= htmlspecialchars($translations['th_methode']) ?></th>
|
||||
<th scope="col" class="text-center"><?= htmlspecialchars($translations['th_statut']) ?></th>
|
||||
<th scope="col" class="text-center"><?= htmlspecialchars($translations['th_actions']) ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($paiements as $p):
|
||||
$methodeRaw = mb_strtolower(trim((string)($p['methode'] ?? '')), 'UTF-8');
|
||||
$methodeNorm = match ($methodeRaw) {
|
||||
'cb' => 'carte',
|
||||
default => $methodeRaw,
|
||||
};
|
||||
$libMethode = $translations['methode_' . $methodeNorm]?? ucfirst($methodeNorm);
|
||||
$libStatut = $statutMap[mb_strtolower($p['statut'] ?? '', 'UTF-8')] ?? ($p['statut'] ?? '');
|
||||
$badgeClass = badgeClassForStatus($p['statut'] ?? '');
|
||||
?>
|
||||
<tr>
|
||||
<td class="text-start"><?= htmlspecialchars($translations['facture_prefix']) ?><?= str_pad((string)$p['id'], 3, '0', STR_PAD_LEFT) ?></td>
|
||||
<td class="text-start"><?= date('d/m/Y', strtotime($p['date_paiement'])) ?></td>
|
||||
<td class="text-start"><?= number_format((float)$p['montant'], 2, ',', ' ') ?> €</td>
|
||||
<td class="text-start"><?= htmlspecialchars($libMethode) ?></td>
|
||||
<td>
|
||||
<span class="badge <?= $badgeClass ?>">
|
||||
<?= htmlspecialchars($libStatut) ?>
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<a href="/public/praticien/factures_details.php?id=<?= (int)$p['id'] ?>" target="_blank" class="btn btn-outline-secondary btn-sm btn-table">
|
||||
<?= htmlspecialchars($translations['btn_voir']) ?>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</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] || '');
|
||||
}
|
||||
});
|
||||
});
|
||||
// ✅ une seule source de vérité
|
||||
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>
|
||||
<!--*****************************FIN Affichage mobile************************-->
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p><?= $translations['copyr'] ?></p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,389 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../includes/init_praticien.php';
|
||||
|
||||
$paiement_id = intval($_GET['id'] ?? 0);
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT p.*, a.nom AS formule
|
||||
FROM paiement p
|
||||
JOIN abonnement a ON p.abonnement_id = a.id
|
||||
WHERE p.id = ? AND p.praticien_id = ?
|
||||
");
|
||||
$stmt->execute([$paiement_id, $_SESSION['reference_id']]);
|
||||
$paiement = $stmt->fetch();
|
||||
|
||||
if (!$paiement) {
|
||||
die(htmlspecialchars($translations['facture_introuvable']));
|
||||
}
|
||||
|
||||
$methodeMap = [
|
||||
'cb' => $translations['methode_carte'] ?? 'CB',
|
||||
'virement' => $translations['methode_virement'] ?? 'Virement',
|
||||
'paypal' => $translations['methode_paypal'] ?? 'PayPal',
|
||||
'stripe' => $translations['methode_stripe'] ?? 'Stripe',
|
||||
];
|
||||
|
||||
$statutMap = [
|
||||
'paye' => $translations['statut_paye'] ?? 'Payé',
|
||||
'payé' => $translations['statut_paye'] ?? 'Payé',
|
||||
'succeeded' => $translations['statut_paye'] ?? 'Payé',
|
||||
'pending' => $translations['statut_en_attente'] ?? 'En attente',
|
||||
'en_attente' => $translations['statut_en_attente'] ?? 'En attente',
|
||||
'failed' => $translations['statut_echoue'] ?? 'Échoué',
|
||||
'echoue' => $translations['statut_echoue'] ?? 'Échoué',
|
||||
'échoué' => $translations['statut_echoue'] ?? 'Échoué',
|
||||
'refunded' => $translations['statut_rembourse'] ?? 'Remboursé',
|
||||
'rembourse' => $translations['statut_rembourse'] ?? 'Remboursé',
|
||||
'remboursé' => $translations['statut_rembourse'] ?? 'Remboursé',
|
||||
];
|
||||
|
||||
$methode = strtolower($paiement['methode'] ?? '');
|
||||
$translationKey = 'methode_' . $methode;
|
||||
$libMethode = $translations[$translationKey] ?? ucfirst($methode);
|
||||
$libStatut = $statutMap[strtolower($paiement['statut'] ?? '')] ?? ($paiement['statut'] ?? '');
|
||||
|
||||
$numero = str_pad((string)$paiement['id'], 6, '0', STR_PAD_LEFT);
|
||||
$datePaiement = !empty($paiement['date_paiement']) ? date('d/m/Y', strtotime($paiement['date_paiement'])) : '';
|
||||
$montant = number_format((float)$paiement['montant'], 2, ',', ' ') . ' €';
|
||||
|
||||
$formuleKey = trim((string)($paiement['formule'] ?? ''));
|
||||
$formuleTxt = $formuleKey !== '' ? ($translations[$formuleKey] ?? $formuleKey) : '';
|
||||
|
||||
$statutLower = strtolower((string)($paiement['statut'] ?? ''));
|
||||
$badgeClass =
|
||||
in_array($statutLower, ['paye','payé','succeeded'], true) ? 'badge--paid' :
|
||||
(in_array($statutLower, ['pending','en_attente'], true) ? 'badge--pending' :
|
||||
(in_array($statutLower, ['failed','echoue','échoué'], true) ? 'badge--failed' :
|
||||
(in_array($statutLower, ['refunded','rembourse','remboursé'], true) ? 'badge--refunded' : 'badge--neutral')));
|
||||
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="<?= htmlspecialchars($_SESSION['lang'] ?? 'fr') ?>">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title><?= htmlspecialchars($translations['titre_facture']) ?> #<?= htmlspecialchars($numero) ?></title>
|
||||
|
||||
<style>
|
||||
:root{
|
||||
--brand:#38378B; /* même vibe que la capture */
|
||||
--bg:#f3f4f6; /* gris clair */
|
||||
--card:#ffffff;
|
||||
--text:#111827;
|
||||
--muted:#6b7280;
|
||||
--line:#d9dde6;
|
||||
--shadow:0 14px 40px rgba(17,24,39,.10);
|
||||
--radius:18px;
|
||||
|
||||
--paid:#16a34a;
|
||||
--pending:#f59e0b;
|
||||
--failed:#dc2626;
|
||||
--refunded:#2563eb;
|
||||
--neutral:#6b7280;
|
||||
|
||||
--btn:#e53935; /* rouge bouton comme capture */
|
||||
--btnHover:#cf2f2b;
|
||||
}
|
||||
|
||||
*{box-sizing:border-box}
|
||||
body{
|
||||
margin:0;
|
||||
font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, "Noto Sans", sans-serif;
|
||||
color:var(--text);
|
||||
background:var(--bg);
|
||||
}
|
||||
|
||||
.wrap{
|
||||
max-width: 980px;
|
||||
margin: 0 auto;
|
||||
padding: 28px 18px 40px;
|
||||
}
|
||||
|
||||
/* Carte principale */
|
||||
.card{
|
||||
background:var(--card);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
overflow:hidden; /* pour arrondi de la bande top */
|
||||
border: 1px solid rgba(17,24,39,.06);
|
||||
}
|
||||
|
||||
/* Bande violette top */
|
||||
.topbar{
|
||||
background: var(--brand);
|
||||
height: 90px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.brand{
|
||||
position:absolute;
|
||||
left: 28px;
|
||||
bottom: 22px;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.brand__logo{
|
||||
height: 46px;
|
||||
width:auto;
|
||||
display:block;
|
||||
filter: drop-shadow(0 2px 0 rgba(0,0,0,.08));
|
||||
}
|
||||
|
||||
/* Contenu */
|
||||
.content{
|
||||
padding: 34px 34px 18px;
|
||||
}
|
||||
|
||||
.title{
|
||||
margin:0 0 14px;
|
||||
font-size: 28px;
|
||||
font-weight: 800;
|
||||
letter-spacing: .2px;
|
||||
}
|
||||
|
||||
.subtitle{
|
||||
margin:0 0 24px;
|
||||
color:var(--muted);
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Ligne en-tête reçu */
|
||||
.meta{
|
||||
display:flex;
|
||||
flex-wrap:wrap;
|
||||
align-items:flex-start;
|
||||
justify-content:space-between;
|
||||
gap: 14px;
|
||||
margin: 6px 0 18px;
|
||||
}
|
||||
|
||||
.receipt-id{
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.receipt-id .k{
|
||||
color:var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
.receipt-id .v{
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
letter-spacing: .6px;
|
||||
}
|
||||
|
||||
.badge{
|
||||
display:inline-flex;
|
||||
align-items:center;
|
||||
gap: 8px;
|
||||
padding: 10px 14px;
|
||||
border-radius: 999px;
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
border: 1px solid rgba(17,24,39,.10);
|
||||
background: #fff;
|
||||
white-space:nowrap;
|
||||
}
|
||||
.dot{
|
||||
width:10px; height:10px; border-radius:999px;
|
||||
background: var(--neutral);
|
||||
}
|
||||
.badge--paid .dot{background:var(--paid)}
|
||||
.badge--pending .dot{background:var(--pending)}
|
||||
.badge--failed .dot{background:var(--failed)}
|
||||
.badge--refunded .dot{background:var(--refunded)}
|
||||
.badge--neutral .dot{background:var(--neutral)}
|
||||
|
||||
/* Bloc infos encadré (comme capture) */
|
||||
.box{
|
||||
border:1px solid var(--line);
|
||||
border-radius: 14px;
|
||||
overflow:hidden;
|
||||
margin: 18px 0 24px;
|
||||
}
|
||||
|
||||
.row{
|
||||
display:grid;
|
||||
grid-template-columns: 260px 1fr;
|
||||
gap: 0;
|
||||
padding: 16px 18px;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
.row:first-child{border-top:none}
|
||||
|
||||
.label{
|
||||
color:var(--muted);
|
||||
font-size: 16px;
|
||||
padding-right: 18px;
|
||||
border-right: 1px solid var(--line);
|
||||
display:flex;
|
||||
align-items:center;
|
||||
min-height: 28px;
|
||||
}
|
||||
.value{
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
padding-left: 18px;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
min-height: 28px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* Total */
|
||||
.total{
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:space-between;
|
||||
gap: 14px;
|
||||
padding: 18px 18px;
|
||||
border-top: 1px solid var(--line);
|
||||
background: rgba(56,55,139,.04);
|
||||
}
|
||||
.total .k{ color:var(--muted); font-weight:700; }
|
||||
.total .v{ font-size: 22px; font-weight: 900; }
|
||||
|
||||
/* Bouton */
|
||||
.actions{
|
||||
display:flex;
|
||||
justify-content:center;
|
||||
padding: 6px 0 18px;
|
||||
}
|
||||
.btn{
|
||||
border:0;
|
||||
background: var(--btn);
|
||||
color:#fff;
|
||||
font-weight: 800;
|
||||
padding: 16px 26px;
|
||||
border-radius: 999px;
|
||||
cursor:pointer;
|
||||
font-size: 16px;
|
||||
box-shadow: 0 10px 22px rgba(229,57,53,.22);
|
||||
transition: transform .05s ease, background .15s ease;
|
||||
}
|
||||
.btn:hover{ background: var(--btnHover); }
|
||||
.btn:active{ transform: translateY(1px); }
|
||||
|
||||
/* Footer carte */
|
||||
.card-footer{
|
||||
padding: 16px 34px 28px;
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
border-top: 1px solid rgba(17,24,39,.06);
|
||||
}
|
||||
|
||||
/* Footer page (comme capture) */
|
||||
.page-footer{
|
||||
text-align:center;
|
||||
color:#9ca3af;
|
||||
padding: 18px 6px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Mobile */
|
||||
@media (max-width: 720px){
|
||||
.topbar{height: 90px}
|
||||
.brand{left: 18px; bottom: 18px}
|
||||
.content{padding: 26px 18px 12px}
|
||||
.card-footer{padding: 14px 18px 22px}
|
||||
.row{grid-template-columns: 1fr; padding: 14px 14px}
|
||||
.label{border-right:none; padding-right:0; margin-bottom:6px}
|
||||
.value{padding-left:0}
|
||||
.title{font-size:24px}
|
||||
}
|
||||
|
||||
/* Impression */
|
||||
@media print{
|
||||
body{background:#fff}
|
||||
.wrap{padding:0; max-width:100%}
|
||||
.card{box-shadow:none; border:1px solid #e5e7eb}
|
||||
.page-footer{display:none}
|
||||
.btn{display:none}
|
||||
.topbar{height: 90px}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<div class="card">
|
||||
<div class="topbar">
|
||||
<div class="brand">
|
||||
<img class="brand__logo" src="/public/uploads/img/logo_mail.png" alt="Logo">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<h1 class="title">
|
||||
<?= htmlspecialchars($translations['titre_facture']) ?>
|
||||
</h1>
|
||||
|
||||
<p class="subtitle">
|
||||
<?= htmlspecialchars($translations['facture_prefix'] ?? 'Reçu') ?>
|
||||
#<?= htmlspecialchars($numero) ?> —
|
||||
<?= htmlspecialchars($translations['facture_date'] ?? 'Date') ?> : <?= htmlspecialchars($datePaiement) ?>
|
||||
</p>
|
||||
|
||||
<div class="meta">
|
||||
<div class="receipt-id">
|
||||
<div class="k"><?= htmlspecialchars($translations['facture_prefix'] ?? 'Reçu') ?></div>
|
||||
<div class="v">#<?= htmlspecialchars($numero) ?></div>
|
||||
</div>
|
||||
|
||||
<div class="badge <?= $badgeClass ?>">
|
||||
<span class="dot"></span>
|
||||
<span><?= htmlspecialchars($libStatut) ?></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<div class="row">
|
||||
<div class="label"><?= htmlspecialchars($translations['facture_date']) ?></div>
|
||||
<div class="value"><?= htmlspecialchars($datePaiement) ?></div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="label"><?= htmlspecialchars($translations['facture_formule']) ?></div>
|
||||
<div class="value"><?= htmlspecialchars($formuleTxt) ?></div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="label"><?= htmlspecialchars($translations['facture_methode']) ?></div>
|
||||
<div class="value"><?= htmlspecialchars($libMethode) ?></div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="label"><?= htmlspecialchars($translations['facture_statut']) ?></div>
|
||||
<div class="value"><?= htmlspecialchars($libStatut) ?></div>
|
||||
</div>
|
||||
|
||||
<div class="total">
|
||||
<div class="k"><?= htmlspecialchars($translations['facture_montant']) ?></div>
|
||||
<div class="v"><?= htmlspecialchars($montant) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button class="btn" onclick="window.print()">
|
||||
<?= htmlspecialchars($translations['btn_imprimer_facture']) ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-footer">
|
||||
<div>
|
||||
<?= htmlspecialchars($translations['facture_note'] ?? 'Merci pour votre paiement. Conservez ce reçu pour vos archives.') ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-footer">
|
||||
© <?= date('Y') ?> УСЛУГИ.ФР — <?= htmlspecialchars($translations['merci'] ?? 'Спасибо.') ?>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../includes/init_praticien.php';
|
||||
|
||||
|
||||
|
||||
// Récupérer les infos du praticien
|
||||
$stmt = $pdo->prepare("SELECT * FROM praticien WHERE id = ?");
|
||||
$stmt->execute([$_SESSION['reference_id'] ?? 0]);
|
||||
$praticien = $stmt->fetch();
|
||||
|
||||
$id = $_SESSION['reference_id'] ?? null;
|
||||
if (!$id) { http_response_code(403); exit('Forbidden'); }
|
||||
require_once __DIR__ . '/../includes/header.php';
|
||||
?>
|
||||
|
||||
<div class="main1">
|
||||
<div class="container py-5">
|
||||
<div class="shadow p-4 rounded bg-light">
|
||||
<h2 class="mb-4 text-center"><?= htmlspecialchars($translations['mes_infos_heading']) ?></h2>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="row g-4 mb-3">
|
||||
<!-- Colonne gauche -->
|
||||
<div class="col-12 col-sm-6">
|
||||
<p><strong><?= htmlspecialchars($translations['label_nom']) ?> :</strong>
|
||||
<?= htmlspecialchars($praticien['nom'] ?? '') ?></p>
|
||||
|
||||
<p><strong><?= htmlspecialchars($translations['label_prenom']) ?> :</strong>
|
||||
<?= htmlspecialchars($praticien['prenom'] ?? '') ?></p>
|
||||
|
||||
<p><strong><?= htmlspecialchars($translations['label_email']) ?> :</strong>
|
||||
<?= htmlspecialchars($praticien['email'] ?? '') ?></p>
|
||||
|
||||
<p><strong><?= htmlspecialchars($translations['label_telephone']) ?> :</strong>
|
||||
<?= htmlspecialchars($praticien['telephone'] ?? '') ?></p>
|
||||
|
||||
<div>
|
||||
<strong><?= htmlspecialchars($translations['alt_photo']) ?> :</strong><br><br>
|
||||
<?php if (!empty($praticien['photo'])): ?>
|
||||
<img
|
||||
src="/public/uploads/<?= htmlspecialchars($praticien['photo']) ?>"
|
||||
alt="<?= htmlspecialchars($translations['alt_photo']) ?>"
|
||||
class="img-fluid rounded-4 shadow"
|
||||
style="max-width:230px;height:auto;">
|
||||
<?php else: ?>
|
||||
<span class="text-muted"><?= htmlspecialchars($translations['aucune_photo'] ?? 'Aucune photo') ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Colonne droite -->
|
||||
<div class="col-12 col-sm-6">
|
||||
<p><strong><?= htmlspecialchars($translations['placeholder_siret']) ?> :</strong>
|
||||
<?= htmlspecialchars($praticien['siret'] ?? '') ?></p>
|
||||
|
||||
<p><strong><?= htmlspecialchars($translations['label_specialite']) ?> :</strong>
|
||||
<?= htmlspecialchars($praticien['specialite'] ?? '') ?></p>
|
||||
|
||||
<!-- Ajout tarif et moyen de paiement -->
|
||||
<p><strong><?= htmlspecialchars($translations['tarif_consultation']) ?> :</strong>
|
||||
<?= htmlspecialchars($praticien['titre'] ?? '') ?></p>
|
||||
|
||||
<p><strong><?= htmlspecialchars($translations['moyens_payement']) ?> :</strong>
|
||||
<?= htmlspecialchars($praticien['disponibilite'] ?? '') ?></p>
|
||||
|
||||
<p><strong><?= htmlspecialchars($translations['label_ville']) ?> :</strong>
|
||||
<?= htmlspecialchars($praticien['ville'] ?? '') ?></p>
|
||||
|
||||
<p><strong><?= htmlspecialchars($translations['label_code_postal']) ?> :</strong>
|
||||
<?= htmlspecialchars($praticien['code_postal'] ?? '') ?></p>
|
||||
|
||||
<p><strong><?= htmlspecialchars($translations['label_adresse']) ?> :</strong><br>
|
||||
<?= nl2br(htmlspecialchars($praticien['adresse'] ?? '')) ?></p>
|
||||
|
||||
<p><strong><?= htmlspecialchars($translations['label_presentation']) ?> :</strong><br>
|
||||
<?= nl2br(htmlspecialchars($praticien['presentation'] ?? '')) ?></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-center">
|
||||
<a href="/public/praticien/modifier.php" class="btn btn-primary">
|
||||
<?= htmlspecialchars($translations['btn_modifier_mes_infos']) ?>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p><?= $translations['copyr'] ?></p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<?php
|
||||
// Si ton header.php ouvre <body>, pense à fermer proprement dans footer.php.
|
||||
// Si tu n'as pas de footer commun, laisse comme ça.
|
||||
?>
|
||||
@@ -0,0 +1,492 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../includes/init_praticien.php';
|
||||
|
||||
|
||||
$id = $_SESSION['reference_id'] ?? null;
|
||||
if (!$id) { http_response_code(403); exit('Forbidden'); }
|
||||
|
||||
// Récupérer les données (pour affichage initial & valeurs par défaut)
|
||||
$stmt = $pdo->prepare("SELECT * FROM praticien WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$praticien = $stmt->fetch();
|
||||
if (!$praticien) {
|
||||
die(htmlspecialchars($translations['praticien_introuvable'] ?? 'Introuvable.'));
|
||||
}
|
||||
|
||||
// ==== Traitement POST avant tout output ====
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$titre = trim($_POST['titre'] ?? '');
|
||||
$nom = trim($_POST['nom'] ?? '');
|
||||
$prenom = trim($_POST['prenom'] ?? '');
|
||||
$telephone = trim($_POST['telephone'] ?? '');
|
||||
$specialite = trim($_POST['specialite'] ?? '');
|
||||
$adresse = trim($_POST['adresse'] ?? '');
|
||||
$ville = trim($_POST['ville'] ?? '');
|
||||
$code_postal = trim($_POST['code_postal'] ?? '');
|
||||
$presentation = trim($_POST['presentation'] ?? '');
|
||||
$duree_rdv = (int)($_POST['duree_rdv'] ?? 60);
|
||||
$disponibilite = trim($_POST['disponibilite'] ?? '');
|
||||
$photo = $praticien['photo'] ?? null;
|
||||
|
||||
|
||||
// --- BLOC UPLOAD SÉCURISÉ ---
|
||||
if (!empty($_FILES['photo']['tmp_name'])) {
|
||||
$file = $_FILES['photo'];
|
||||
$maxSize = 5 * 1024 * 1024; // 5 Mo
|
||||
|
||||
// 1. Vérification de la taille
|
||||
if ($file['size'] > $maxSize) {
|
||||
die("Erreur : Le fichier est trop volumineux (max 5 Mo).");
|
||||
}
|
||||
|
||||
// 2. Vérification du type réel (MIME) pour bloquer les faux fichiers
|
||||
$finfo = new finfo(FILEINFO_MIME_TYPE);
|
||||
$mimeType = $finfo->file($file['tmp_name']);
|
||||
$allowedMimeTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
||||
|
||||
if (!in_array($mimeType, $allowedMimeTypes)) {
|
||||
die("Erreur : Type de fichier non autorisé (Seuls JPG, PNG, GIF et WEBP sont acceptés).");
|
||||
}
|
||||
|
||||
// 3. Préparation du nom et du chemin
|
||||
$nom_nettoye = strtolower(preg_replace('/[^A-Za-z0-9]/', '', $nom));
|
||||
$prenom_nettoye = strtolower(preg_replace('/[^A-Za-z0-9]/', '', $prenom));
|
||||
$nouveau_nom_fichier = $nom_nettoye . '_' . $prenom_nettoye . '_' . time() . '.png';
|
||||
$chemin_destination = __DIR__ . '/../uploads/' . $nouveau_nom_fichier;
|
||||
|
||||
// 4. Traitement de l'image (Conversion & Redimensionnement)
|
||||
$image_source = @imagecreatefromstring(file_get_contents($file['tmp_name']));
|
||||
|
||||
if ($image_source) {
|
||||
list($largeur_orig, $hauteur_orig) = getimagesize($file['tmp_name']);
|
||||
$largeur_max = 800;
|
||||
|
||||
if ($largeur_orig > $largeur_max) {
|
||||
$ratio = $largeur_max / $largeur_orig;
|
||||
$nouvelle_largeur = $largeur_max;
|
||||
$nouvelle_hauteur = (int)($hauteur_orig * $ratio);
|
||||
} else {
|
||||
$nouvelle_largeur = $largeur_orig;
|
||||
$nouvelle_hauteur = $hauteur_orig;
|
||||
}
|
||||
|
||||
$image_dest = imagecreatetruecolor($nouvelle_largeur, $nouvelle_hauteur);
|
||||
imagealphablending($image_dest, false);
|
||||
imagesavealpha($image_dest, true);
|
||||
|
||||
imagecopyresampled($image_dest, $image_source, 0, 0, 0, 0, $nouvelle_largeur, $nouvelle_hauteur, $largeur_orig, $hauteur_orig);
|
||||
|
||||
// Sauvegarde effective
|
||||
if (imagepng($image_dest, $chemin_destination, 7)) {
|
||||
// Suppression de l'ancienne photo si elle existe
|
||||
if (!empty($praticien['photo'])) {
|
||||
$ancien_chemin = __DIR__ . '/../uploads/' . $praticien['photo'];
|
||||
if (file_exists($ancien_chemin)) {
|
||||
unlink($ancien_chemin);
|
||||
}
|
||||
}
|
||||
$photo = $nouveau_nom_fichier;
|
||||
}
|
||||
|
||||
imagedestroy($image_source);
|
||||
imagedestroy($image_dest);
|
||||
} else {
|
||||
die("Erreur : Le fichier image semble corrompu.");
|
||||
}
|
||||
}
|
||||
|
||||
// SIRET - normalisation + vérifs
|
||||
$siret = preg_replace('/\D/', '', $_POST['siret'] ?? '');
|
||||
if (strlen($siret) !== 14) {
|
||||
die(htmlspecialchars($translations['siret_format_invalide'] ?? 'SIRET invalide (14 chiffres).'));
|
||||
}
|
||||
// Luhn
|
||||
$luhnOk = (function($num){
|
||||
$sum=0; $alt=false;
|
||||
for ($i=strlen($num)-1; $i>=0; $i--){
|
||||
$n=(int)$num[$i];
|
||||
if($alt){ $n*=2; if($n>9)$n-=9; }
|
||||
$sum+=$n; $alt=!$alt;
|
||||
}
|
||||
return $sum%10===0;
|
||||
})($siret);
|
||||
if (!$luhnOk) {
|
||||
die(htmlspecialchars($translations['siret_format_invalide'] ?? 'SIRET invalide (Luhn).'));
|
||||
}
|
||||
|
||||
// Unicité SIRET (exclure l'utilisateur courant)
|
||||
$chk = $pdo->prepare("SELECT id FROM praticien WHERE siret = :s AND id <> :id LIMIT 1");
|
||||
$chk->execute(['s' => $siret, 'id' => $id]);
|
||||
if ($chk->fetch()) {
|
||||
die(htmlspecialchars($translations['err_siret_deja_utilise'] ?? 'Ce SIRET est déjà utilisé.'));
|
||||
}
|
||||
|
||||
// Mise à jour
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE praticien SET
|
||||
titre = :titre,
|
||||
nom = :nom,
|
||||
prenom = :prenom,
|
||||
telephone = :telephone,
|
||||
specialite = :specialite,
|
||||
adresse = :adresse,
|
||||
ville = :ville,
|
||||
code_postal = :code_postal,
|
||||
presentation = :presentation,
|
||||
photo = :photo,
|
||||
disponibilite = :disponibilite,
|
||||
duree_rdv = :duree_rdv,
|
||||
siret = :siret
|
||||
WHERE id = :id
|
||||
");
|
||||
$stmt->execute([
|
||||
'titre' => $titre,
|
||||
'nom' => $nom,
|
||||
'prenom' => $prenom,
|
||||
'telephone' => $telephone,
|
||||
'specialite' => $specialite,
|
||||
'adresse' => $adresse,
|
||||
'ville' => $ville,
|
||||
'code_postal' => $code_postal,
|
||||
'presentation' => $presentation,
|
||||
'photo' => $photo,
|
||||
'disponibilite' => $disponibilite,
|
||||
'duree_rdv' => $duree_rdv,
|
||||
'siret' => $siret,
|
||||
'id' => $id,
|
||||
]);
|
||||
|
||||
// Redirection ABSOLUE (ou "informations.php" si même dossier)
|
||||
header('Location: /public/praticien/informations.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$PAGE_TITLE = $translations['modifier_infos_title'];
|
||||
$HEAD_EXTRA = <<<HTML
|
||||
<style>
|
||||
.message-erreur {
|
||||
color: red;
|
||||
font-size: 0.9em;
|
||||
display: none;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
HTML;
|
||||
// ==== À partir d’ici, on peut afficher ====
|
||||
require_once __DIR__ . '/../includes/header.php';
|
||||
?>
|
||||
|
||||
<div class="main1">
|
||||
<div class="container py-5">
|
||||
<form id="form-praticien-edit" method="POST" enctype="multipart/form-data" class="shadow p-4 rounded bg-light">
|
||||
<h2 class="text-center mb-4"><?= htmlspecialchars($translations['modifier_infos_title']) ?></h2>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= htmlspecialchars($translations['label_nom']) ?> *</label>
|
||||
<input type="text" name="nom" class="form-control" value="<?= htmlspecialchars($praticien['nom']?? '') ?>" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= htmlspecialchars($translations['label_prenom']) ?> *</label>
|
||||
<input type="text" name="prenom" class="form-control" value="<?= htmlspecialchars($praticien['prenom']?? '') ?>" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= htmlspecialchars($translations['label_telephone']) ?> *</label>
|
||||
<input type="tel" name="telephone" id="telephone" class="form-control" value="<?= htmlspecialchars($praticien['telephone']?? '') ?>" required placeholder="+33600000000"
|
||||
|
||||
pattern="^\+?[1-9]\d{6,14}$"
|
||||
title="<?=htmlspecialchars($translations['telephone_format_message']?? '') ?>">
|
||||
</div>
|
||||
<!-- Champ SIREN/SIRET -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= htmlspecialchars($translations['placeholder_siret']) ?> *</label>
|
||||
<input type="text"
|
||||
name="siret"
|
||||
id="siret"
|
||||
class="form-control"
|
||||
value="<?= htmlspecialchars($praticien['siret']?? '') ?>"
|
||||
required
|
||||
maxlength="17"
|
||||
inputmode="numeric"
|
||||
pattern="[0-9 ]+">
|
||||
<div id="siret-feedback" class="message-erreur" style="display:none;"></div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= htmlspecialchars($translations['label_specialite']) ?> *</label>
|
||||
<input type="text" name="specialite" class="form-control" value="<?= htmlspecialchars($praticien['specialite']?? '') ?>" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= htmlspecialchars($translations['tarif_consultation']) ?></label>
|
||||
<input type="text" name="titre" class="form-control" value="<?= htmlspecialchars($praticien['titre']?? '') ?>">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= htmlspecialchars($translations['moyens_payement']) ?></label>
|
||||
<input type="text" name="disponibilite" class="form-control" value="<?= htmlspecialchars($praticien['disponibilite']?? '') ?>">
|
||||
</div>
|
||||
|
||||
<div class="mb-3 position-relative">
|
||||
<label class="form-label"><?= htmlspecialchars($translations['label_adresse']) ?></label>
|
||||
<textarea name="adresse" id="adresse" class="form-control"><?= htmlspecialchars($praticien['adresse']?? '') ?></textarea>
|
||||
<ul id="suggestions"></ul>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= htmlspecialchars($translations['label_ville']) ?></label>
|
||||
<input type="text" name="ville" id="ville" class="form-control" value="<?= htmlspecialchars($praticien['ville']?? '') ?>">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= htmlspecialchars($translations['label_code_postal']) ?></label>
|
||||
<input type="text" name="code_postal" id="code_postal" class="form-control" value="<?= htmlspecialchars($praticien['code_postal']?? '') ?>">
|
||||
</div>
|
||||
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= htmlspecialchars($translations['label_presentation']) ?></label>
|
||||
<textarea name="presentation" class="form-control"><?= htmlspecialchars($praticien['presentation']?? '') ?></textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= htmlspecialchars($translations['label_duree_rdv']) ?></label>
|
||||
<input type="number" name="duree_rdv" class="form-control" value="<?= $praticien['duree_rdv'] ?>" min="15" max="180" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= htmlspecialchars($translations['label_photo']) ?></label>
|
||||
<input type="file" name="photo" class="form-control" accept="image/*">
|
||||
</div>
|
||||
|
||||
<?php if ($praticien['photo']): ?>
|
||||
<div class="mb-3">
|
||||
<p><?= htmlspecialchars($translations['photo_actuelle']) ?></p>
|
||||
<img src="/public/uploads/<?= htmlspecialchars($praticien['photo']) ?>" class="img-fluid rounded-4 shadow" style="width:230px;height:auto;">
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="mt-4 text-center">
|
||||
<button type="submit" class="btn btn-primary"><?= htmlspecialchars($translations['btn_enregistrer']) ?></button>
|
||||
|
||||
</div>
|
||||
<?= $translations['champs_obligatoire'] ?>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
<script>
|
||||
// === Téléphone: simple & efficace ===
|
||||
const tel = document.getElementById('telephone');
|
||||
|
||||
// 1) Nettoyage en direct (espaces, tirets, points, parenthèses)
|
||||
tel?.addEventListener('input', function () {
|
||||
const pos = this.selectionStart;
|
||||
this.value = this.value.replace(/[\s\-._()/]/g, '');
|
||||
try { this.setSelectionRange(pos, pos); } catch(_) {}
|
||||
});
|
||||
|
||||
// 2) Normalisation au blur
|
||||
tel?.addEventListener('blur', function() {
|
||||
let val = this.value.trim().replace(/[\s\-._()/]/g, '');
|
||||
if (val.startsWith('00')) val = '+' + val.slice(2);
|
||||
if (/^0\d{9}$/.test(val)) val = '+33' + val.substring(1);
|
||||
if (val && !val.startsWith('+')) val = '+' + val;
|
||||
this.value = val;
|
||||
});
|
||||
|
||||
// 3) Au submit : applique la normalisation même si l’utilisateur n’a pas quitté le champ
|
||||
tel?.form?.addEventListener('submit', function (e) {
|
||||
tel.dispatchEvent(new Event('blur'));
|
||||
if (!tel.checkValidity()) {
|
||||
e.preventDefault();
|
||||
tel.reportValidity();
|
||||
tel.focus();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const adresseInput = document.getElementById("adresse");
|
||||
const villeInput = document.getElementById("ville");
|
||||
const cpInput = document.getElementById("code_postal");
|
||||
const suggestions = document.getElementById("suggestions");
|
||||
|
||||
// petite fonction utilitaire : construit "numero + rue"
|
||||
function buildStreet(props) {
|
||||
const num = (props.housenumber || '').trim();
|
||||
const street = (props.street || props.name || '').trim();
|
||||
return [num, street].filter(Boolean).join(' ');
|
||||
}
|
||||
// fallback ville si city manquant (rare)
|
||||
function getCity(props) {
|
||||
if (props.city) return props.city;
|
||||
// ex: "84160, Cucuron, Vaucluse, Provence-Alpes-Côte d'Azur"
|
||||
if (props.context) {
|
||||
const parts = props.context.split(',').map(s => s.trim());
|
||||
// généralement la 2e valeur correspond à la commune
|
||||
return parts[1] || parts[0] || '';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
let abortCtrl = null;
|
||||
|
||||
adresseInput.addEventListener("input", function () {
|
||||
const query = adresseInput.value.trim();
|
||||
if (query.length < 3) {
|
||||
suggestions.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// annule la requête précédente si on retape vite
|
||||
try { abortCtrl?.abort(); } catch(_) {}
|
||||
abortCtrl = new AbortController();
|
||||
|
||||
fetch("https://api-adresse.data.gouv.fr/search/?limit=5&q=" + encodeURIComponent(query), {
|
||||
signal: abortCtrl.signal
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
suggestions.innerHTML = '';
|
||||
(data.features || []).forEach(feature => {
|
||||
const props = feature.properties || {};
|
||||
const li = document.createElement("li");
|
||||
li.className = 'list-group-item list-group-item-action';
|
||||
li.textContent = props.label || buildStreet(props);
|
||||
|
||||
li.addEventListener("click", function () {
|
||||
// ✅ Adresse = seulement numéro + rue
|
||||
adresseInput.value = buildStreet(props);
|
||||
// ✅ Ville + CP séparés
|
||||
villeInput.value = getCity(props);
|
||||
cpInput.value = props.postcode || '';
|
||||
suggestions.innerHTML = '';
|
||||
});
|
||||
|
||||
suggestions.appendChild(li);
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
});
|
||||
|
||||
// clic hors zone -> ferme la liste
|
||||
document.addEventListener("click", function (e) {
|
||||
if (!suggestions.contains(e.target) && e.target !== adresseInput) {
|
||||
suggestions.innerHTML = '';
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<!--***********************************************************
|
||||
Verification ajax pour SIRET
|
||||
**************************************************************-->
|
||||
<script>
|
||||
// Luhn SIRET
|
||||
function luhnCheck(num) {
|
||||
const arr = (num + '').split('').reverse().map(x => parseInt(x, 10));
|
||||
const sum = arr.reduce((acc, val, i) => {
|
||||
if (i % 2) { val *= 2; if (val > 9) val -= 9; }
|
||||
return acc + val;
|
||||
}, 0);
|
||||
return (sum % 10) === 0;
|
||||
}
|
||||
|
||||
(function(){
|
||||
const form = document.getElementById('form-praticien-edit'); // <-- bon id
|
||||
const siretInput = document.getElementById('siret');
|
||||
const feedback = document.getElementById('siret-feedback');
|
||||
|
||||
// init
|
||||
form.dataset.siretExists = '0';
|
||||
let debounceId = null;
|
||||
let lastQueried = '';
|
||||
|
||||
const MSG = {
|
||||
invalide: '<?= addslashes($translations["siret_format_invalide"] ?? "Numéro SIRET invalide.") ?>',
|
||||
valide: '<?= addslashes($translations["siret_valide_format"] ?? "Format SIRET valide.") ?>',
|
||||
existe: '<?= addslashes($translations["siret_trouve"] ?? "Numéro reconnu dans la base INSEE.") ?>',
|
||||
nexiste: '<?= addslashes($translations["siret_non_trouve"] ?? "Numéro introuvable dans la base INSEE.") ?>',
|
||||
attente: '<?= addslashes($translations["siret_verification_en_cours"] ?? "Vérification en cours…") ?>'
|
||||
};
|
||||
|
||||
function formatSiret(value) {
|
||||
value = value.replace(/\D/g, '');
|
||||
if (value.length > 3) value = value.slice(0,3) + ' ' + value.slice(3);
|
||||
if (value.length > 7) value = value.slice(0,7) + ' ' + value.slice(7);
|
||||
if (value.length > 11) value = value.slice(0,11) + ' ' + value.slice(11);
|
||||
return value;
|
||||
}
|
||||
function setFeedback(text, color) {
|
||||
feedback.textContent = text;
|
||||
feedback.style.color = color;
|
||||
feedback.style.display = 'block';
|
||||
}
|
||||
|
||||
// Pré-formate la valeur initiale si elle est brute (14 chiffres)
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (/^\d{14}$/.test(siretInput.value)) {
|
||||
siretInput.value = formatSiret(siretInput.value);
|
||||
}
|
||||
});
|
||||
|
||||
function checkSiretImmediate() {
|
||||
// formatage visuel
|
||||
const formatted = formatSiret(siretInput.value);
|
||||
if (siretInput.value !== formatted) siretInput.value = formatted;
|
||||
|
||||
const raw = siretInput.value.replace(/\s+/g, '');
|
||||
|
||||
if (raw.length === 0) { feedback.style.display = 'none'; form.dataset.siretExists = '0'; return; }
|
||||
if (raw.length !== 14) { setFeedback(MSG.invalide, 'red'); form.dataset.siretExists = '0'; return; }
|
||||
if (!luhnCheck(raw)) { setFeedback(MSG.invalide, 'red'); form.dataset.siretExists = '0'; return; }
|
||||
|
||||
setFeedback(MSG.attente, '#666');
|
||||
form.dataset.siretExists = '0';
|
||||
|
||||
if (raw === lastQueried) return;
|
||||
lastQueried = raw;
|
||||
|
||||
clearTimeout(debounceId);
|
||||
debounceId = setTimeout(() => {
|
||||
fetch('/../../js/ajax_verif_siret.php?siret=' + encodeURIComponent(raw)) // <-- chemin ABSOLU
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data && data.exists) {
|
||||
setFeedback(MSG.existe, 'green');
|
||||
form.dataset.siretExists = '1';
|
||||
} else {
|
||||
setFeedback(MSG.nexiste, 'red');
|
||||
form.dataset.siretExists = '0';
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setFeedback(MSG.nexiste, 'red');
|
||||
form.dataset.siretExists = '0';
|
||||
});
|
||||
}, 500);
|
||||
}
|
||||
|
||||
siretInput.addEventListener('input', checkSiretImmediate);
|
||||
|
||||
// Normalise AVANT envoi (supprime les espaces)
|
||||
form.addEventListener('submit', function(e){
|
||||
const raw = siretInput.value.replace(/\s+/g, '');
|
||||
if (raw.length !== 14 || !luhnCheck(raw)) {
|
||||
e.preventDefault();
|
||||
setFeedback(MSG.invalide, 'red');
|
||||
siretInput.focus();
|
||||
return;
|
||||
}
|
||||
siretInput.value = raw; // <- on poste 14 chiffres à PHP
|
||||
// Si tu veux forcer l’existence INSEE côté front, décommente :
|
||||
// if (form.dataset.siretExists !== '1') { e.preventDefault(); setFeedback(MSG.nexiste, 'red'); siretInput.focus(); }
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
</div>
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p><?= $translations['copyr'] ?></p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,315 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../includes/init_praticien.php';
|
||||
|
||||
require_once '../includes/header.php';
|
||||
$pdo->prepare("
|
||||
INSERT INTO push_badge_counters (compte_id, msg_unread, rdv_unread, updated_at)
|
||||
VALUES (:cid, 0, 0, NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
rdv_unread = 0,
|
||||
updated_at = NOW()
|
||||
")->execute([':cid' => (int)$_SESSION['compte_id']]);
|
||||
|
||||
|
||||
$praticien_id = $_SESSION['reference_id'];
|
||||
$id_rdv = isset($_GET['id']) ? intval($_GET['id']) : null;
|
||||
|
||||
// Suppression si demandée (via POST confirmé par SweetAlert2 côté client)
|
||||
if ($id_rdv && isset($_POST['delete'])) {
|
||||
$stmt = $pdo->prepare("DELETE FROM rendez_vous
|
||||
WHERE id = ? AND statut = 'annulé' AND praticien_id = ?");
|
||||
$stmt->execute([$id_rdv, $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>'.htmlspecialchars($translations['rdv_supprime'] ?? 'Rendez-vous supprimé').'</h4>';
|
||||
echo ' </div>';
|
||||
echo ' <a href="/public/praticien/rendez_vous.php" class="btn btn-primary">';
|
||||
echo ' <i class="bi bi-arrow-left-circle"></i> '.htmlspecialchars($translations['retour_a_la_liste'] ?? 'Retour à la liste').'
|
||||
</a>';
|
||||
echo ' </div>';
|
||||
echo '</div>';
|
||||
echo '</div>';
|
||||
exit;
|
||||
}
|
||||
|
||||
// Charger les rendez-vous
|
||||
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 = ? AND rv.praticien_id = ?
|
||||
");
|
||||
$stmt->execute([$id_rdv, $praticien_id]);
|
||||
$rdvs = $stmt->fetchAll();
|
||||
} 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 = ?
|
||||
ORDER BY rv.date_rdv DESC
|
||||
");
|
||||
$stmt->execute([$praticien_id]);
|
||||
$rdvs = $stmt->fetchAll();
|
||||
}
|
||||
|
||||
// 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_TITLE = $translations['mes_rdv_title'] ?? 'Mes rendez-vous';
|
||||
$HEAD_EXTRA = <<<HTML
|
||||
<script src="/js/sweetalert2@11.js"></script>
|
||||
HTML;
|
||||
|
||||
?>
|
||||
<div class="main1">
|
||||
<div class="container py-5">
|
||||
<div class="shadow p-4 rounded bg-light">
|
||||
<h2 class="mb-4 text-center"><?= htmlspecialchars($translations['mes_rdv_title'] ?? 'Mes rendez-vous') ?></h2>
|
||||
|
||||
<?php if (!$id_rdv): ?>
|
||||
<a href="/public/praticien/rendez_vous_du_jour.php" class="btn btn-outline-secondary mb-4">
|
||||
<?= htmlspecialchars($translations['voir_rdv_du_jour'] ?? 'Voir les RDV du jour') ?>
|
||||
</a>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="filtre_statut" class="form-label"><?= htmlspecialchars($translations['filtrer_par_statut'] ?? 'Filtrer par statut') ?></label>
|
||||
<select id="filtre_statut" class="form-select w-auto d-inline-block" onchange="filtrer()">
|
||||
<option value=""><?= htmlspecialchars($translations['option_tous'] ?? 'Tous') ?></option>
|
||||
<?php foreach (['prévu','confirmé','annulé','terminé'] as $s): ?>
|
||||
<option value="<?= $s ?>"><?= htmlspecialchars($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><?= htmlspecialchars($translations['col_date'] ?? 'Date') ?></th>
|
||||
<th><?= htmlspecialchars($translations['col_heure'] ?? 'Heure') ?></th>
|
||||
<th><?= htmlspecialchars($translations['col_client'] ?? 'Client') ?></th>
|
||||
<th><?= htmlspecialchars($translations['col_duree'] ?? 'Durée') ?></th>
|
||||
<th><?= htmlspecialchars($translations['col_statut'] ?? 'Statut') ?></th>
|
||||
<?php if (!$id_rdv): ?><th><?= htmlspecialchars($translations['col_action'] ?? 'Action') ?></th><?php endif; ?>
|
||||
<th><?= htmlspecialchars($translations['btn_chat'] ?? 'Chat') ?></th>
|
||||
<th><?= htmlspecialchars($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="<?= htmlspecialchars($r['statut']) ?>">
|
||||
<td><?= htmlspecialchars($dt->format('d/m/Y')) ?></td>
|
||||
<td><?= htmlspecialchars($dt->format('H:i')) ?></td>
|
||||
<td><?= htmlspecialchars($r['prenom_client'] . ' ' . $r['nom_client']) ?></td>
|
||||
<td><?= (int)$r['duree_minutes'] . ' ' . htmlspecialchars($translations['min_abbr'] ?? 'min') ?></td>
|
||||
<td>
|
||||
<select class="form-select form-select-sm" onchange="changerStatut(<?= $id ?>, this.value)">
|
||||
<?php foreach (['prévu','confirmé','annulé','terminé'] as $s): ?>
|
||||
<option value="<?= $s ?>" <?= ($r['statut'] === $s ? 'selected' : '') ?>>
|
||||
<?= htmlspecialchars($statutMap[$s] ?? ucfirst($s)) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</td>
|
||||
|
||||
<?php if (!$id_rdv): ?>
|
||||
<td>
|
||||
<?php if ($r['statut'] === 'annulé'): ?>
|
||||
<form method="POST" action="?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">
|
||||
<?= htmlspecialchars($translations['btn_supprimer'] ?? 'Supprimer') ?>
|
||||
</button>
|
||||
</form>
|
||||
<?php else: ?>
|
||||
<a href="?id=<?= $id ?>" class="btn btn-outline-secondary btn-sm btn-table">
|
||||
<?= htmlspecialchars($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'] ?>">
|
||||
<?= htmlspecialchars($translations['chat_praticien'] ?? 'Ouvrir le chat') ?>
|
||||
</a>
|
||||
</td>
|
||||
|
||||
<!-- ====== Colonne Visio / Cabinet ====== -->
|
||||
<td>
|
||||
<?php
|
||||
$lieu = strtolower(trim((string)($r['lieu'] ?? '')));
|
||||
if ($lieu === 'online') {
|
||||
// --- on garde l'ancienne logique visio telle quelle ---
|
||||
$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">
|
||||
<?= htmlspecialchars($translations['visio_nachalo'] ?? 'Démarrer la visio') ?>
|
||||
</a>
|
||||
<?php
|
||||
} else {
|
||||
?>
|
||||
<small class="text-muted d-block mt-1">
|
||||
<?php
|
||||
if ($now < $open) echo htmlspecialchars(($translations['ouvert_a'] ?? 'Ouvert à') . ' ' . $open->format('H:i'));
|
||||
elseif ($now > $close) echo htmlspecialchars(($translations['ferme_a'] ?? 'Fermé à') . ' ' . $close->format('H:i'));
|
||||
else echo htmlspecialchars(($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">
|
||||
<?= htmlspecialchars($translations['votre_rdv_jdet'] ?? 'En attente de confirmation') ?>
|
||||
</small>
|
||||
<?php
|
||||
} else {
|
||||
?>
|
||||
<small class="text-muted d-block mt-1">
|
||||
<?= htmlspecialchars(($translations['zaverchen_a'] ?? 'Terminé') . ' ' . $close->format('H:i')) ?>
|
||||
</small>
|
||||
<?php
|
||||
}
|
||||
} else {
|
||||
// --- offline : juste afficher "Очно" / libellé cabinet ---
|
||||
echo '<span class="text-nowrap">'.htmlspecialchars($translations['option_cabinet'] ?? 'Очно').'</span>';
|
||||
}
|
||||
?>
|
||||
</td>
|
||||
<!-- ===================================== -->
|
||||
</tr>
|
||||
<?php endforeach; else: ?>
|
||||
<tr>
|
||||
<td colspan="<?= $id_rdv ? 6 : 8 ?>"><?= htmlspecialchars($translations['aucun_rdv_trouve'] ?? 'Aucun rendez-vous trouvé') ?></td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<?php if ($id_rdv): ?>
|
||||
<a href="/public/praticien/rendez_vous.php" class="btn btn-outline-secondary mb-4">
|
||||
<?= htmlspecialchars($translations['retour_a_la_liste'] ?? 'Retour à la liste') ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function changerStatut(id, statut) {
|
||||
fetch('/public/praticien/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) })
|
||||
})
|
||||
.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,240 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../includes/init_praticien.php';
|
||||
|
||||
$praticien_id = $_SESSION['reference_id'];
|
||||
|
||||
// --- Today 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');
|
||||
|
||||
$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 = ?
|
||||
AND rv.date_rdv BETWEEN ? AND ?
|
||||
ORDER BY rv.date_rdv ASC
|
||||
");
|
||||
$stmt->execute([$praticien_id, $today_start, $today_end]);
|
||||
$rdvs = $stmt->fetchAll();
|
||||
|
||||
// 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_TITLE = $translations['titre_rdv_du_jour'] ?? 'Rendez-vous du jour';
|
||||
$HEAD_EXTRA = <<<HTML
|
||||
<script src="/js/sweetalert2@11.js"></script>
|
||||
HTML;
|
||||
require_once '../includes/header.php';
|
||||
?>
|
||||
<div class="main1">
|
||||
<div class="container py-5">
|
||||
<div class="shadow p-4 rounded bg-light">
|
||||
<h2 class="mb-4 text-center"><?= htmlspecialchars($translations['titre_rdv_du_jour'] ?? 'Rendez-vous du jour') ?></h2>
|
||||
|
||||
<a href="/public/praticien/rendez_vous.php" class="btn btn-outline-secondary mb-4">
|
||||
<?= htmlspecialchars($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><?= htmlspecialchars($translations['col_heure'] ?? 'Heure') ?></th>
|
||||
<th><?= htmlspecialchars($translations['col_client'] ?? 'Client') ?></th>
|
||||
<th><?= htmlspecialchars($translations['col_duree'] ?? 'Durée') ?></th>
|
||||
<th><?= htmlspecialchars($translations['col_statut'] ?? 'Statut') ?></th>
|
||||
<th><?= htmlspecialchars($translations['btn_chat'] ?? 'Chat') ?></th>
|
||||
<th><?= htmlspecialchars($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="<?= htmlspecialchars($r['statut']) ?>">
|
||||
<td><?= htmlspecialchars($dt->format('H:i')) ?></td>
|
||||
<td><?= htmlspecialchars($r['prenom_client'] . ' ' . $r['nom_client']) ?></td>
|
||||
<td><?= (int)$r['duree_minutes'] . ' ' . htmlspecialchars($translations['min_abbr'] ?? 'min') ?></td>
|
||||
<td>
|
||||
<select class="form-select form-select-sm" onchange="changerStatut(<?= $id ?>, this.value, this)">
|
||||
<?php foreach (['prévu','confirmé','annulé','terminé'] as $s): ?>
|
||||
<option value="<?= $s ?>" <?= ($r['statut'] === $s ? 'selected' : '') ?>>
|
||||
<?= htmlspecialchars($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'] ?>">
|
||||
<?= htmlspecialchars($translations['chat_praticien'] ?? 'Ouvrir le chat') ?>
|
||||
</a>
|
||||
</td>
|
||||
|
||||
<!-- Visio / Cabinet -->
|
||||
<td>
|
||||
<?php
|
||||
$lieu = strtolower(trim((string)($r['lieu'] ?? '')));
|
||||
if ($lieu === 'online') {
|
||||
// LOGIQUE VISIO (inchangée)
|
||||
$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">
|
||||
<?= htmlspecialchars($translations['visio_nachalo'] ?? 'Démarrer la visio') ?>
|
||||
</a>
|
||||
<?php
|
||||
} else {
|
||||
?>
|
||||
<small class="text-muted d-block mt-1">
|
||||
<?php
|
||||
if ($now < $open) echo htmlspecialchars(($translations['ouvert_a'] ?? 'Ouvert à') . ' ' . $open->format('H:i'));
|
||||
elseif ($now > $close) echo htmlspecialchars(($translations['ferme_a'] ?? 'Fermé à') . ' ' . $close->format('H:i'));
|
||||
else echo htmlspecialchars(($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">
|
||||
<?= htmlspecialchars($translations['votre_rdv_jdet'] ?? 'En attente de confirmation') ?>
|
||||
</small>
|
||||
<?php
|
||||
} else {
|
||||
?>
|
||||
<small class="text-muted d-block mt-1">
|
||||
<?= htmlspecialchars(($translations['zaverchen_a'] ?? 'Terminé') . ' ' . $close->format('H:i')) ?>
|
||||
</small>
|
||||
<?php
|
||||
}
|
||||
} else {
|
||||
// OFFLINE : afficher juste le libellé "Очно"
|
||||
echo '<span class="text-nowrap">'.htmlspecialchars($translations['option_cabinet'] ?? 'Очно').'</span>';
|
||||
}
|
||||
?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if (empty($rdvs)): ?>
|
||||
<tr>
|
||||
<td colspan="6" class="text-muted">
|
||||
<?= htmlspecialchars($translations['aucun_rdv_aujourdhui'] ?? "Aucun rendez-vous aujourd'hui") ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
// --- Mise à jour du statut avec SweetAlert2 ---
|
||||
// On gère JSON *et* fallback texte "OK" si l'endpoint n'est pas encore JSON.
|
||||
function changerStatut(id, statut, selEl) {
|
||||
fetch('/public/praticien/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) })
|
||||
})
|
||||
.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 (_) {
|
||||
// Fallback: si l'API renvoie juste "OK" ou un message en clair
|
||||
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 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(){
|
||||
const tbl = document.getElementById('table_rdv') || document.querySelector('table.mobile-cards');
|
||||
initMobileCards(tbl);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<!--*****************************FIN Affichage mobile************************-->
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p><?= $translations['copyr'] ?? '© '.date('Y') ?></p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../includes/init_praticien.php';
|
||||
|
||||
|
||||
$praticien_id = $_SESSION['reference_id'] ?? null;
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT a.nom, a.prix, a.duree_jours, p.date_expiration
|
||||
FROM praticien p
|
||||
JOIN abonnement a ON p.abonnement_id = a.id
|
||||
WHERE p.id = ?
|
||||
");
|
||||
$stmt->execute([$praticien_id]);
|
||||
$abonnement = $stmt->fetch();
|
||||
|
||||
$nomKey = trim((string)($abonnement['nom'] ?? ''));
|
||||
$nomTxt = $translations[$nomKey] ?? $nomKey;
|
||||
require_once '../includes/header.php';
|
||||
?>
|
||||
|
||||
<div class="main1">
|
||||
<div class="container mt-5">
|
||||
<div class="shadow p-4 rounded bg-light">
|
||||
<h3 class="mb-4 text-center"><?= htmlspecialchars($translations['titre_abonnement']) ?></h3>
|
||||
|
||||
<?php if ($abonnement):
|
||||
$dateExp = date('d/m/Y', strtotime($abonnement['date_expiration']));
|
||||
?>
|
||||
<div class="mb-4">
|
||||
<p><strong><?= htmlspecialchars($translations['facture_formule']) ?> :</strong> <?= $nomTxt ?></p>
|
||||
<p><strong><?= htmlspecialchars($translations['label_date_expiration']) ?> :</strong> <?= $dateExp ?></p>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center">
|
||||
<button type="button" class="btn btn-outline-primary custom-gray ms-3" data-bs-toggle="modal" data-bs-target="#modalResiliation">
|
||||
<?= htmlspecialchars($translations['btn_resilier_abonnement']) ?>
|
||||
</button>
|
||||
<a href="/public/choisir_abonnement.php" class="btn btn-outline-primary custom-gray ms-3">
|
||||
<?= htmlspecialchars($translations['btn_changer_formule']) ?>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- MODALE -->
|
||||
<div class="modal fade" id="modalResiliation" tabindex="-1" aria-labelledby="modalResiliationLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content border-danger">
|
||||
<div class="modal-header bg-danger text-white">
|
||||
<h5 class="modal-title" id="modalResiliationLabel">
|
||||
<?= htmlspecialchars($translations['modal_resiliation_titre']) ?>
|
||||
</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Fermer"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
⚠️ <?= htmlspecialchars($translations['modal_resiliation_texte']) ?><br>
|
||||
<?php
|
||||
$texteAcces = strtr($translations['modal_resiliation_acces_jusqua'], ['{date}' => $dateExp]);
|
||||
?>
|
||||
<?= $texteAcces ?>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<form method="POST" action="/public/praticien/resiliation_action.php">
|
||||
<button type="submit" class="btn btn-danger">
|
||||
<?= htmlspecialchars($translations['btn_oui_resilier']) ?>
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
<?= htmlspecialchars($translations['btn_annuler']) ?>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php else: ?>
|
||||
<div class="alert alert-warning">
|
||||
<?= htmlspecialchars($translations['abonnement_absent']) ?>
|
||||
</div>
|
||||
<a href="/public/choisir_abonnement.php" class="btn btn-primary">
|
||||
<?= htmlspecialchars($translations['btn_choisir_abonnement']) ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bootstrap JS (si non inclus ailleurs) -->
|
||||
<script src="/js/bootstrap.bundle.min.js"></script>
|
||||
</div>
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p><?= $translations['copyr'] ?></p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
// /public/resiliation_action.php
|
||||
require_once __DIR__ . '/../includes/init_praticien.php';
|
||||
|
||||
|
||||
if (!isset($pdo) || !($pdo instanceof PDO)) {
|
||||
http_response_code(500);
|
||||
exit('DB_ERROR');
|
||||
}
|
||||
|
||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
|
||||
|
||||
$praticien_id = (int)($_SESSION['reference_id'] ?? 0);
|
||||
if ($praticien_id <= 0) {
|
||||
header('Location: /public/login.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// ---- Helper: colonne existe ? (évite de casser selon schéma) ----
|
||||
function colExists(PDO $pdo, string $table, string $col): bool {
|
||||
static $cache = [];
|
||||
$k = $table.'.'.$col;
|
||||
if (array_key_exists($k, $cache)) return $cache[$k];
|
||||
$st = $pdo->prepare("
|
||||
SELECT 1
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = ?
|
||||
AND COLUMN_NAME = ?
|
||||
LIMIT 1
|
||||
");
|
||||
$st->execute([$table, $col]);
|
||||
$cache[$k] = (bool)$st->fetchColumn();
|
||||
return $cache[$k];
|
||||
}
|
||||
|
||||
try {
|
||||
// 🔎 Récupération du billing le plus récent "utile"
|
||||
// On vise l’entrée la plus récente; si elle est déjà canceled, ça reste idempotent.
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT id, billing_mode, provider, provider_subscription_id, current_period_end, status
|
||||
FROM billing_subscriptions
|
||||
WHERE praticien_id = ?
|
||||
ORDER BY id DESC
|
||||
LIMIT 1
|
||||
");
|
||||
$stmt->execute([$praticien_id]);
|
||||
$billing = $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
|
||||
|
||||
// ==========================
|
||||
// 1) Appel Stripe (si auto)
|
||||
// ==========================
|
||||
if ($billing) {
|
||||
$isAutoStripe = (
|
||||
(string)($billing['billing_mode'] ?? '') === 'auto'
|
||||
&& (string)($billing['provider'] ?? '') === 'stripe'
|
||||
&& !empty($billing['provider_subscription_id'])
|
||||
);
|
||||
|
||||
if ($isAutoStripe) {
|
||||
require_once __DIR__ . '/../../plata/lib/stripe_client.php'; // $stripe
|
||||
|
||||
try {
|
||||
// Résiliation à fin de période
|
||||
$stripe->subscriptions->update(
|
||||
(string)$billing['provider_subscription_id'],
|
||||
['cancel_at_period_end' => true]
|
||||
);
|
||||
} catch (Throwable $e) {
|
||||
// On ne bloque pas la résiliation locale si Stripe échoue
|
||||
@file_put_contents(
|
||||
'/tmp/stripe_resiliation.log',
|
||||
'['.date('c').'] '.$e->getMessage()."\n",
|
||||
FILE_APPEND
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================
|
||||
// 2) Mise à jour locale (TX)
|
||||
// ==========================
|
||||
$pdo->beginTransaction();
|
||||
|
||||
// 2.1) billing_subscriptions -> canceled (+ flags si colonnes existent)
|
||||
if ($billing && !empty($billing['id'])) {
|
||||
$sets = ["status = 'canceled'"];
|
||||
$vals = [];
|
||||
|
||||
if (colExists($pdo, 'billing_subscriptions', 'cancel_at_period_end')) {
|
||||
$sets[] = "cancel_at_period_end = 1";
|
||||
}
|
||||
if (colExists($pdo, 'billing_subscriptions', 'canceled_at')) {
|
||||
$sets[] = "canceled_at = NOW()";
|
||||
}
|
||||
|
||||
$sql = "UPDATE billing_subscriptions SET ".implode(', ', $sets)." WHERE id = ?";
|
||||
$vals[] = (int)$billing['id'];
|
||||
|
||||
$pdo->prepare($sql)->execute($vals);
|
||||
}
|
||||
|
||||
// 2.2) praticien -> abonnement_id NULL (NE PAS toucher date_expiration)
|
||||
if (colExists($pdo, 'praticien', 'abonnement_id')) {
|
||||
$pdo->prepare("UPDATE praticien SET abonnement_id = NULL WHERE id = ?")
|
||||
->execute([$praticien_id]);
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
} catch (Throwable $e) {
|
||||
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||
@file_put_contents(
|
||||
'/tmp/resiliation_error.log',
|
||||
'['.date('c').'] '.$e->getMessage()."\n",
|
||||
FILE_APPEND
|
||||
);
|
||||
}
|
||||
|
||||
// Redirection finale
|
||||
header('Location: /public/praticien/resiliation.php?ok=1');
|
||||
exit;
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../includes/init_praticien.php';
|
||||
require_once '../includes/header.php';
|
||||
if ($_SESSION['role'] === 'praticien'): ?>
|
||||
|
||||
<div class="main1">
|
||||
<div class="container py-5">
|
||||
<div class="shadow p-4 rounded bg-light">
|
||||
<h2 class="mb-4 text-center"><?= htmlspecialchars($translations['titre_sync_agenda']) ?></h2>
|
||||
|
||||
<!-- Google Agenda -->
|
||||
<div class="card_sync p-4">
|
||||
<h4><?= htmlspecialchars($translations['google_import_title']) ?></h4>
|
||||
<p><?= htmlspecialchars($translations['google_import_text']) ?></p>
|
||||
<a href="/google/connect_google.php">
|
||||
<button class="btn btn-outline-secondary mb-4"><?= htmlspecialchars($translations['btn_connect_google_agenda']) ?></button>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- iCal -->
|
||||
<div class="card_sync p-4">
|
||||
<h4><?= htmlspecialchars($translations['ical_import_title']) ?></h4>
|
||||
<p><?= htmlspecialchars($translations['ical_import_text']) ?></p>
|
||||
<a href="https://uslugi.fr/calendar.php?id=<?= (int)$_SESSION['reference_id'] ?>" target="_blank" rel="noopener">
|
||||
<button class="btn btn-outline-secondary mb-4"><?= htmlspecialchars($translations['btn_add_to_ical']) ?></button>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Outlook -->
|
||||
<div class="card_sync p-4">
|
||||
<h4><?= htmlspecialchars($translations['outlook_import_title']) ?></h4>
|
||||
<p><?= htmlspecialchars($translations['outlook_import_text']) ?></p>
|
||||
<a href="https://uslugi.fr/outlook_connect.php" target="_blank" rel="noopener">
|
||||
<button class="btn btn-outline-secondary mb-4"><?= htmlspecialchars($translations['btn_connect_outlook']) ?></button>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p><?= $translations['copyr'] ?></p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
<?php endif; ?>
|
||||
Reference in new issue
Block a user