Files
maria_uslugi/public/praticien/clients/index.php
T
2026-03-05 10:25:39 +01:00

279 lines
11 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
require_once __DIR__ . '/../../includes/init_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>