Init Git Prod Mars 2026

This commit is contained in:
Oleg Barykin committed 2026-03-05 10:25:39 +01:00
commit d81efe93b1
362 files changed
+86627

No files matched your search

+235
View File
@@ -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>
+649
View File
@@ -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');
}
+108
View File
@@ -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');
}
+104
View File
@@ -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>
+31
View File
@@ -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);
+171
View File
@@ -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>
+287
View File
@@ -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>
+278
View File
@@ -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=>({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;' }[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>
+160
View File
@@ -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>
+283
View File
@@ -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'] ?? '&copy; ' . date('Y') ?></p>
</div>
</footer>
</body>
</html>
+249
View File
@@ -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'] ?? '&copy; ' . date('Y') ?></p>
</div>
</footer>
</body>
</html>
+47
View File
@@ -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);
+135
View File
@@ -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>
+389
View File
@@ -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>
+100
View File
@@ -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.
?>
+492
View File
@@ -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>
+315
View File
@@ -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'] ?? '&copy; '.date('Y') ?></p>
</div>
</footer>
</body>
</html>
+240
View File
@@ -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'] ?? '&copy; '.date('Y') ?></p>
</div>
</footer>
</body>
</html>
+95
View File
@@ -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>
+123
View File
@@ -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;
+48
View File
@@ -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; ?>