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

+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>