Init Git Prod Mars 2026
This commit is contained in:
commit
d81efe93b1
362 files changed
+86627
No files matched your search
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../includes/init_praticien.php';
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
function out($ok,$msg,$extra=[]){ echo json_encode(['ok'=>$ok,'message'=>$msg]+$extra); exit; }
|
||||
|
||||
$praticien_id = (int)($_SESSION['reference_id'] ?? 0);
|
||||
$csrf = (string)($_POST['csrf'] ?? '');
|
||||
if (!hash_equals($_SESSION['cl_csrf'] ?? '', $csrf)) out(false, 'CSRF');
|
||||
|
||||
$dossier_id = (int)($_POST['dossier_id'] ?? 0);
|
||||
if ($dossier_id<=0) out(false, 'Bad dossier');
|
||||
|
||||
// Owner
|
||||
$st = $pdo->prepare("SELECT d.id, d.client_id FROM client_dossiers d WHERE d.id=:d AND d.praticien_id=:p AND d.archived_at IS NULL");
|
||||
$st->execute([':d'=>$dossier_id, ':p'=>$praticien_id]);
|
||||
$dos = $st->fetch(PDO::FETCH_ASSOC);
|
||||
if(!$dos) out(false, 'Not allowed');
|
||||
|
||||
$date_rdv = (string)($_POST['date_rdv'] ?? '');
|
||||
$type_rdv = (string)($_POST['type_rdv'] ?? 'autre');
|
||||
$titre = trim((string)($_POST['titre'] ?? ''));
|
||||
$notes = trim((string)($_POST['notes'] ?? ''));
|
||||
$prochaine= trim((string)($_POST['prochaine_action'] ?? ''));
|
||||
$duree = (int)($_POST['duree_minutes'] ?? 0);
|
||||
|
||||
if ($titre==='' || $notes==='') out(false, 'Champs requis');
|
||||
|
||||
try{
|
||||
$pdo->beginTransaction();
|
||||
$ins = $pdo->prepare("INSERT INTO client_suivi (dossier_id, rdv_id, date_rdv, type_rdv, titre, notes, prochaine_action, duree_minutes, prive)
|
||||
VALUES (:d, NULL, :dt, :ty, :ti, :no, :pa, :du, 1)");
|
||||
$ins->execute([
|
||||
':d'=>$dossier_id,
|
||||
':dt'=> (new DateTime($date_rdv ?: 'now'))->format('Y-m-d H:i:s'),
|
||||
':ty'=> in_array($type_rdv,['online','cabinet','tel','autre'],true) ? $type_rdv : 'autre',
|
||||
':ti'=> $titre,
|
||||
':no'=> $notes,
|
||||
':pa'=> ($prochaine !== '' ? $prochaine : null),
|
||||
':du'=> ($duree > 0 ? $duree : null),
|
||||
]);
|
||||
$entry_id = (int)$pdo->lastInsertId();
|
||||
|
||||
// Upload files
|
||||
if (!empty($_FILES['files']) && is_array($_FILES['files']['name'])) {
|
||||
$baseDir = $_SERVER['DOCUMENT_ROOT'].'/uploads/suivi/'. $dossier_id;
|
||||
if (!is_dir($baseDir)) @mkdir($baseDir, 0775, true);
|
||||
|
||||
$allowed = ['pdf','jpg','jpeg','png','doc','docx','txt'];
|
||||
for($i=0;$i<count($_FILES['files']['name']);$i++){
|
||||
$err = (int)($_FILES['files']['error'][$i] ?? UPLOAD_ERR_NO_FILE);
|
||||
if ($err !== UPLOAD_ERR_OK) continue;
|
||||
$name = (string)($_FILES['files']['name'][$i] ?? '');
|
||||
$tmp = (string)($_FILES['files']['tmp_name'][$i] ?? '');
|
||||
$size = (int)($_FILES['files']['size'][$i] ?? 0);
|
||||
if ($size <= 0 || $size > 8*1024*1024) continue; // 8 Mo
|
||||
$ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
|
||||
if (!in_array($ext,$allowed,true)) continue;
|
||||
|
||||
$clean = preg_replace('~[^a-zA-Z0-9._-]+~','_', $name);
|
||||
$new = $dossier_id.'_'.time().'_'.bin2hex(random_bytes(4)).'.'.$ext;
|
||||
$dest = $baseDir.'/'.$new;
|
||||
if (@move_uploaded_file($tmp, $dest)) {
|
||||
$rel = '/uploads/suivi/'.$dossier_id.'/'.$new;
|
||||
$mime= mime_content_type($dest) ?: null;
|
||||
|
||||
$pdo->prepare("INSERT INTO client_suivi_files (entry_id, path, mime, size_bytes) VALUES (:e,:p,:m,:s)")
|
||||
->execute([':e'=>$entry_id, ':p'=>$rel, ':m'=>$mime, ':s'=>$size]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
out(true, $translations['cl_enregistre_ok'] ?? 'Note enregistrée.');
|
||||
}catch(Throwable $e){
|
||||
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||
error_log('ajax_ajouter_note: '.$e->getMessage());
|
||||
out(false, $translations['cl_enregistre_ko'] ?? 'Erreur lors de l’enregistrement.');
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../includes/init_praticien.php';
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
function out($ok,$m,$x=[]){ echo json_encode(['ok'=>$ok,'message'=>$m]+$x); exit; }
|
||||
|
||||
$praticien_id = (int)($_SESSION['reference_id'] ?? 0);
|
||||
$csrf=(string)($_POST['csrf'] ?? '');
|
||||
if (!hash_equals($_SESSION['cl_csrf'] ?? '', $csrf)) out(false,'CSRF');
|
||||
|
||||
$note_id = (int)($_POST['note_id'] ?? 0);
|
||||
if ($note_id<=0) out(false,'Bad note');
|
||||
|
||||
// Vérif propriétaire + dossier_id
|
||||
$sql = "SELECT s.id, s.dossier_id, d.praticien_id
|
||||
FROM client_suivi s JOIN client_dossiers d ON d.id=s.dossier_id
|
||||
WHERE s.id=:id AND d.praticien_id=:p AND d.archived_at IS NULL";
|
||||
$st = $pdo->prepare($sql);
|
||||
$st->execute([':id'=>$note_id, ':p'=>$praticien_id]);
|
||||
$own = $st->fetch(PDO::FETCH_ASSOC);
|
||||
if(!$own) out(false,'Not allowed');
|
||||
|
||||
$date_rdv = (string)($_POST['date_rdv'] ?? '');
|
||||
$type_rdv = (string)($_POST['type_rdv'] ?? 'autre');
|
||||
$titre = trim((string)($_POST['titre'] ?? ''));
|
||||
$notes = trim((string)($_POST['notes'] ?? ''));
|
||||
$prochaine= trim((string)($_POST['prochaine_action'] ?? ''));
|
||||
$duree = (int)($_POST['duree_minutes'] ?? 0);
|
||||
|
||||
if ($titre==='' || $notes==='') out(false,'Champs requis');
|
||||
|
||||
try{
|
||||
$pdo->beginTransaction();
|
||||
|
||||
$upd = $pdo->prepare("UPDATE client_suivi
|
||||
SET date_rdv=:dt, type_rdv=:ty, titre=:ti, notes=:no, prochaine_action=:pa, duree_minutes=:du
|
||||
WHERE id=:id");
|
||||
$upd->execute([
|
||||
':dt'=> (new DateTime($date_rdv ?: 'now'))->format('Y-m-d H:i:s'),
|
||||
':ty'=> in_array($type_rdv,['online','cabinet','tel','autre'],true) ? $type_rdv : 'autre',
|
||||
':ti'=> $titre, ':no'=> $notes,
|
||||
':pa'=> ($prochaine !== '' ? $prochaine : null),
|
||||
':du'=> ($duree > 0 ? $duree : null),
|
||||
':id'=> $note_id
|
||||
]);
|
||||
|
||||
// Uploads additionnels
|
||||
if (!empty($_FILES['files']) && is_array($_FILES['files']['name'])) {
|
||||
$baseDir = $_SERVER['DOCUMENT_ROOT'].'/uploads/suivi/'. (int)$own['dossier_id'];
|
||||
if (!is_dir($baseDir)) @mkdir($baseDir, 0775, true);
|
||||
|
||||
$allowed = ['pdf','jpg','jpeg','png','doc','docx','txt'];
|
||||
for($i=0;$i<count($_FILES['files']['name']);$i++){
|
||||
$err = (int)($_FILES['files']['error'][$i] ?? UPLOAD_ERR_NO_FILE);
|
||||
if ($err !== UPLOAD_ERR_OK) continue;
|
||||
$name = (string)($_FILES['files']['name'][$i] ?? '');
|
||||
$tmp = (string)($_FILES['files']['tmp_name'][$i] ?? '');
|
||||
$size = (int)($_FILES['files']['size'][$i] ?? 0);
|
||||
if ($size <= 0 || $size > 8*1024*1024) continue; // 8 Mo
|
||||
$ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
|
||||
if (!in_array($ext,$allowed,true)) continue;
|
||||
|
||||
$new = (int)$own['dossier_id'].'_'.time().'_'.bin2hex(random_bytes(4)).'.'.$ext;
|
||||
$dest = $baseDir.'/'.$new;
|
||||
if (@move_uploaded_file($tmp, $dest)) {
|
||||
$rel = '/uploads/suivi/'.(int)$own['dossier_id'].'/'.$new;
|
||||
$mime= mime_content_type($dest) ?: null;
|
||||
|
||||
$pdo->prepare("INSERT INTO client_suivi_files (entry_id, path, mime, size_bytes) VALUES (:e,:p,:m,:s)")
|
||||
->execute([':e'=>$note_id, ':p'=>$rel, ':m'=>$mime, ':s'=>$size]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
out(true,'OK');
|
||||
}catch(Throwable $e){
|
||||
if($pdo->inTransaction()) $pdo->rollBack();
|
||||
error_log('ajax_editer_note: '.$e->getMessage());
|
||||
out(false,'Erreur');
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../includes/init_praticien.php';
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
|
||||
header('Pragma: no-cache');
|
||||
|
||||
$praticien_id = (int)($_SESSION['reference_id'] ?? 0);
|
||||
if ($praticien_id <= 0) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['ok'=>false,'message'=>'Unauthorized']); exit;
|
||||
}
|
||||
|
||||
$qRaw = trim((string)($_GET['q'] ?? ''));
|
||||
$hasQ = ($qRaw !== '');
|
||||
$qLike = '%'.$qRaw.'%';
|
||||
|
||||
$page = max(1, (int)($_GET['page'] ?? 1));
|
||||
$limit = 10;
|
||||
$offset= ($page-1)*$limit;
|
||||
|
||||
try {
|
||||
// ----- TOTAL
|
||||
$sqlTotal = "SELECT COUNT(*) FROM client_dossiers d
|
||||
JOIN client c ON c.id=d.client_id
|
||||
WHERE d.praticien_id=:p AND d.archived_at IS NULL";
|
||||
$paramsT = [':p'=>$praticien_id];
|
||||
if ($hasQ) {
|
||||
$sqlTotal .= " AND (
|
||||
c.nom LIKE :q_nom
|
||||
OR c.prenom LIKE :q_prenom
|
||||
OR c.email LIKE :q_email
|
||||
OR c.telephone LIKE :q_tel
|
||||
OR d.tags LIKE :q_tags
|
||||
)";
|
||||
$paramsT[':q_nom'] = $qLike;
|
||||
$paramsT[':q_prenom'] = $qLike;
|
||||
$paramsT[':q_email'] = $qLike;
|
||||
$paramsT[':q_tel'] = $qLike;
|
||||
$paramsT[':q_tags'] = $qLike;
|
||||
}
|
||||
|
||||
$stT = $pdo->prepare($sqlTotal);
|
||||
foreach ($paramsT as $k=>$v) {
|
||||
$stT->bindValue($k, $v, $k===':p' ? PDO::PARAM_INT : PDO::PARAM_STR);
|
||||
}
|
||||
$stT->execute();
|
||||
$total = (int)$stT->fetchColumn();
|
||||
|
||||
// ----- PAGE
|
||||
$sql = "SELECT d.id AS dossier_id, d.tags,
|
||||
c.nom, c.prenom, c.email, c.telephone,
|
||||
(SELECT MAX(rv.date_rdv) FROM rendez_vous rv
|
||||
WHERE rv.client_id=d.client_id AND rv.praticien_id=d.praticien_id
|
||||
AND rv.statut IN ('confirmé','terminé')) AS last_rdv,
|
||||
(SELECT LEFT(s.prochaine_action, 120) FROM client_suivi s
|
||||
WHERE s.dossier_id=d.id ORDER BY s.date_rdv DESC, s.id DESC LIMIT 1) AS last_note
|
||||
FROM client_dossiers d
|
||||
JOIN client c ON c.id=d.client_id
|
||||
WHERE d.praticien_id=:p AND d.archived_at IS NULL";
|
||||
$params = [':p'=>$praticien_id];
|
||||
if ($hasQ) {
|
||||
$sql .= " AND (
|
||||
c.nom LIKE :q_nom
|
||||
OR c.prenom LIKE :q_prenom
|
||||
OR c.email LIKE :q_email
|
||||
OR c.telephone LIKE :q_tel
|
||||
OR d.tags LIKE :q_tags
|
||||
)";
|
||||
$params[':q_nom'] = $qLike;
|
||||
$params[':q_prenom'] = $qLike;
|
||||
$params[':q_email'] = $qLike;
|
||||
$params[':q_tel'] = $qLike;
|
||||
$params[':q_tags'] = $qLike;
|
||||
}
|
||||
|
||||
// tri: d'abord ceux qui ont un last_rdv, puis par date desc, puis maj dossier
|
||||
$sql .= " ORDER BY (last_rdv IS NULL) ASC, last_rdv DESC, d.updated_at DESC
|
||||
LIMIT :lim OFFSET :off";
|
||||
|
||||
$st = $pdo->prepare($sql);
|
||||
foreach ($params as $k=>$v) {
|
||||
if ($k === ':p') $st->bindValue($k, $v, PDO::PARAM_INT);
|
||||
else $st->bindValue($k, $v, PDO::PARAM_STR);
|
||||
}
|
||||
$st->bindValue(':lim', $limit, PDO::PARAM_INT);
|
||||
$st->bindValue(':off', $offset, PDO::PARAM_INT);
|
||||
$st->execute();
|
||||
|
||||
$items=[];
|
||||
while($r=$st->fetch(PDO::FETCH_ASSOC)){
|
||||
$items[]=[
|
||||
'dossier_id' => (int)$r['dossier_id'],
|
||||
'tags' => $r['tags'],
|
||||
'client_name' => trim(($r['prenom'] ?? '').' '.($r['nom'] ?? '')),
|
||||
'email' => $r['email'] ?? '',
|
||||
'telephone' => $r['telephone'] ?? '',
|
||||
'last_rdv_fmt' => !empty($r['last_rdv']) ? (new DateTime($r['last_rdv']))->format('d/m/Y H:i') : '',
|
||||
'last_note' => $r['last_note'] ?? ''
|
||||
];
|
||||
}
|
||||
|
||||
echo json_encode(['ok'=>true,'items'=>$items,'total'=>$total,'limit'=>$limit]);
|
||||
} catch (Throwable $e) {
|
||||
error_log('clients/ajax_list error: '.$e->getMessage());
|
||||
http_response_code(500);
|
||||
echo json_encode(['ok'=>false,'message'=>'Erreur serveur']);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../includes/init_praticien.php';
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
function out($ok,$msg,$extra=[]){ echo json_encode(['ok'=>$ok,'message'=>$msg]+$extra); exit; }
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') out(false,'Méthode invalide');
|
||||
|
||||
$praticien_id = (int)($_SESSION['reference_id'] ?? 0);
|
||||
$csrf = (string)($_POST['csrf'] ?? '');
|
||||
if (!$praticien_id || !hash_equals($_SESSION['cl_csrf'] ?? '', $csrf)) out(false,'CSRF/Unauthorized');
|
||||
|
||||
$dossier_id = (int)($_POST['dossier_id'] ?? 0);
|
||||
if ($dossier_id <= 0) out(false,'Requête invalide');
|
||||
|
||||
// Vérif propriété du dossier
|
||||
$own = $pdo->prepare("SELECT 1 FROM client_dossiers WHERE id=:d AND praticien_id=:p AND archived_at IS NULL");
|
||||
$own->execute([':d'=>$dossier_id, ':p'=>$praticien_id]);
|
||||
if (!$own->fetchColumn()) out(false,'Accès refusé');
|
||||
|
||||
try{
|
||||
// Récupérer chemins des fichiers pour suppression disque après commit
|
||||
$filePaths = [];
|
||||
$sf = $pdo->prepare("SELECT f.path
|
||||
FROM client_suivi_files f
|
||||
JOIN client_suivi s ON s.id=f.entry_id
|
||||
WHERE s.dossier_id=:d");
|
||||
$sf->execute([':d'=>$dossier_id]);
|
||||
while($r = $sf->fetch(PDO::FETCH_ASSOC)){
|
||||
$filePaths[] = (string)$r['path'];
|
||||
}
|
||||
|
||||
// Récup IDs des notes
|
||||
$noteIds = [];
|
||||
$sn = $pdo->prepare("SELECT id FROM client_suivi WHERE dossier_id=:d");
|
||||
$sn->execute([':d'=>$dossier_id]);
|
||||
while($r = $sn->fetch(PDO::FETCH_ASSOC)){
|
||||
$noteIds[] = (int)$r['id'];
|
||||
}
|
||||
|
||||
$pdo->beginTransaction();
|
||||
|
||||
if ($noteIds) {
|
||||
$ph = implode(',', array_fill(0, count($noteIds), '?'));
|
||||
$delF = $pdo->prepare("DELETE FROM client_suivi_files WHERE entry_id IN ($ph)");
|
||||
$delF->execute($noteIds);
|
||||
|
||||
$delN = $pdo->prepare("DELETE FROM client_suivi WHERE id IN ($ph)");
|
||||
$delN->execute($noteIds);
|
||||
}
|
||||
|
||||
$delD = $pdo->prepare("DELETE FROM client_dossiers WHERE id=:d");
|
||||
$delD->execute([':d'=>$dossier_id]);
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
// Suppression physique (best-effort)
|
||||
$root = rtrim((string)$_SERVER['DOCUMENT_ROOT'],'/');
|
||||
foreach ($filePaths as $rel) {
|
||||
$abs = $root.$rel;
|
||||
if (is_file($abs)) @unlink($abs);
|
||||
}
|
||||
|
||||
out(true,'Dossier supprimé');
|
||||
|
||||
}catch(Throwable $e){
|
||||
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||
error_log('ajax_supprimer_dossier: '.$e->getMessage());
|
||||
out(false,'Erreur serveur');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../includes/init_praticien.php';
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
function out($ok,$m){ echo json_encode(['ok'=>$ok,'message'=>$m]); exit; }
|
||||
|
||||
$praticien_id = (int)($_SESSION['reference_id'] ?? 0);
|
||||
$csrf=(string)($_POST['csrf'] ?? '');
|
||||
if (!hash_equals($_SESSION['cl_csrf'] ?? '', $csrf)) out(false,'CSRF');
|
||||
|
||||
$file_id = (int)($_POST['file_id'] ?? 0);
|
||||
if ($file_id<=0) out(false,'Bad file');
|
||||
|
||||
$sql = "SELECT f.id, f.path
|
||||
FROM client_suivi_files f
|
||||
JOIN client_suivi s ON s.id=f.entry_id
|
||||
JOIN client_dossiers d ON d.id=s.dossier_id
|
||||
WHERE f.id=:id AND d.praticien_id=:p AND d.archived_at IS NULL";
|
||||
$st = $pdo->prepare($sql);
|
||||
$st->execute([':id'=>$file_id, ':p'=>$praticien_id]);
|
||||
$row = $st->fetch(PDO::FETCH_ASSOC);
|
||||
if(!$row) out(false,'Not allowed');
|
||||
|
||||
$pdo->beginTransaction();
|
||||
$pdo->prepare("DELETE FROM client_suivi_files WHERE id=:id")->execute([':id'=>$file_id]);
|
||||
$pdo->commit();
|
||||
|
||||
// Supprime le fichier disque (best-effort)
|
||||
$abs = rtrim((string)$_SERVER['DOCUMENT_ROOT'],'/').(string)$row['path'];
|
||||
if (is_file($abs)) @unlink($abs);
|
||||
|
||||
out(true,'OK');
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../includes/init_praticien.php';
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
function out($ok, $msg, $extra = []) {
|
||||
echo json_encode(['ok'=>$ok, 'message'=>$msg] + $extra);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') out(false, 'Méthode invalide');
|
||||
|
||||
$praticien_id = (int)($_SESSION['reference_id'] ?? 0);
|
||||
$csrf = (string)($_POST['csrf'] ?? '');
|
||||
if (!$praticien_id || !hash_equals($_SESSION['cl_csrf'] ?? '', $csrf)) {
|
||||
out(false, 'CSRF/Unauthorized');
|
||||
}
|
||||
|
||||
$note_id = (int)($_POST['note_id'] ?? 0);
|
||||
if ($note_id <= 0) out(false, 'Requête invalide');
|
||||
|
||||
try {
|
||||
// Vérifier que la note appartient bien au praticien
|
||||
$check = $pdo->prepare("
|
||||
SELECT s.id, s.dossier_id
|
||||
FROM client_suivi s
|
||||
JOIN client_dossiers d ON d.id = s.dossier_id
|
||||
WHERE s.id = :id AND d.praticien_id = :p AND d.archived_at IS NULL
|
||||
LIMIT 1
|
||||
");
|
||||
$check->execute([':id'=>$note_id, ':p'=>$praticien_id]);
|
||||
$own = $check->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$own) out(false, 'Accès refusé');
|
||||
|
||||
// Récupérer la liste des fichiers pour suppression disque après commit
|
||||
$files = [];
|
||||
$sf = $pdo->prepare("SELECT id, path FROM client_suivi_files WHERE entry_id = :e");
|
||||
$sf->execute([':e'=>$note_id]);
|
||||
while ($row = $sf->fetch(PDO::FETCH_ASSOC)) {
|
||||
$files[] = (string)$row['path'];
|
||||
}
|
||||
|
||||
$pdo->beginTransaction();
|
||||
|
||||
// Supprimer lignes fichiers BDD puis la note
|
||||
$pdo->prepare("DELETE FROM client_suivi_files WHERE entry_id = :e")->execute([':e'=>$note_id]);
|
||||
$pdo->prepare("DELETE FROM client_suivi WHERE id = :id")->execute([':id'=>$note_id]);
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
// Suppression physique (best-effort) après commit
|
||||
$root = rtrim((string)$_SERVER['DOCUMENT_ROOT'], '/');
|
||||
foreach ($files as $rel) {
|
||||
$abs = $root.$rel;
|
||||
if (is_file($abs)) @unlink($abs);
|
||||
}
|
||||
|
||||
out(true, 'Note supprimée');
|
||||
|
||||
} catch (Throwable $e) {
|
||||
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||
error_log('ajax_supprimer_note: '.$e->getMessage());
|
||||
out(false, 'Erreur serveur');
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../includes/init_praticien.php';
|
||||
|
||||
require_once __DIR__ . '/../../includes/header.php';
|
||||
function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
|
||||
|
||||
$praticien_id = (int)($_SESSION['reference_id'] ?? 0);
|
||||
$dossier_id = (int)($_GET['dossier_id'] ?? 0);
|
||||
if ($dossier_id<=0) { header('Location: index.php'); exit; }
|
||||
|
||||
// Vérif propriétaire
|
||||
$ok = $pdo->prepare("SELECT d.id, d.praticien_id, c.nom, c.prenom
|
||||
FROM client_dossiers d JOIN client c ON c.id=d.client_id
|
||||
WHERE d.id=:d AND d.praticien_id=:p AND d.archived_at IS NULL");
|
||||
$ok->execute([':d'=>$dossier_id, ':p'=>$praticien_id]);
|
||||
$dos = $ok->fetch(PDO::FETCH_ASSOC);
|
||||
if(!$dos){ header('Location: index.php'); exit; }
|
||||
|
||||
// CSRF
|
||||
if (empty($_SESSION['cl_csrf'])) $_SESSION['cl_csrf'] = bin2hex(random_bytes(32));
|
||||
$CSRF = $_SESSION['cl_csrf'];
|
||||
|
||||
?>
|
||||
<div class="main1">
|
||||
<div class="container py-5">
|
||||
<div class="shadow p-4 rounded bg-light">
|
||||
<main class="container my-4">
|
||||
<div class="card p-3 shadow-sm">
|
||||
<h4 class="mb-3"><?= h($translations['cl_ajouter_note_titre'] ?? 'Ajouter une note') ?></h4>
|
||||
|
||||
<form id="note-form" enctype="multipart/form-data" autocomplete="off">
|
||||
<input type="hidden" name="csrf" value="<?= h($CSRF) ?>">
|
||||
<input type="hidden" name="dossier_id" value="<?= (int)$dossier_id ?>">
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-md-4">
|
||||
<label class="form-label"><?= h($translations['cl_date_rdv'] ?? 'Date') ?></label>
|
||||
<input type="datetime-local" name="date_rdv" class="form-control" value="<?= h((new DateTime())->format('Y-m-d\TH:i')) ?>">
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<label class="form-label"><?= h($translations['cl_type_rdv'] ?? 'Type') ?></label>
|
||||
<select class="form-select" name="type_rdv">
|
||||
<option value="online"><?= h($translations['cl_type_online'] ?? 'En ligne') ?></option>
|
||||
<option value="cabinet"><?= h($translations['cl_type_cabinet'] ?? 'Au cabinet') ?></option>
|
||||
<option value="tel"><?= h($translations['cl_type_tel'] ?? 'Téléphone') ?></option>
|
||||
<option value="autre"><?= h($translations['cl_type_autre'] ?? 'Autre') ?></option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<label class="form-label"><?= h($translations['cl_duree'] ?? 'Durée (min)') ?></label>
|
||||
<input type="number" name="duree_minutes" class="form-control" min="1" max="600" value="60">
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label"><?= h($translations['cl_titre'] ?? 'Titre') ?></label>
|
||||
<input type="text" name="titre" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label"><?= h($translations['cl_notes'] ?? 'Notes') ?></label>
|
||||
<textarea name="notes" class="form-control" rows="6" required></textarea>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label"><?= h($translations['cl_prochaine_action'] ?? 'Prochaine action') ?></label>
|
||||
<input type="text" name="prochaine_action" class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label"><?= h($translations['cl_pieces_jointes'] ?? 'Pièces jointes') ?></label>
|
||||
<input type="file" name="files[]" class="form-control" multiple
|
||||
accept=".pdf,.jpg,.jpeg,.png,.doc,.docx,.txt">
|
||||
<div class="form-text">Max 8 Mo par fichier.</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 d-flex justify-content-end gap-2">
|
||||
<a href="fiche.php?dossier_id=<?= (int)$dossier_id ?>" class="btn btn-outline-secondary"><?= h($translations['cl_pagination_prec'] ?? 'Annuler') ?></a>
|
||||
<button class="btn btn-primary" type="submit"><?= h($translations['cl_enregistrer'] ?? 'Enregistrer') ?></button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p><?= $translations['copyr'] ?></p>
|
||||
</div>
|
||||
</footer>
|
||||
<script>
|
||||
document.getElementById('note-form').addEventListener('submit', async (e)=>{
|
||||
e.preventDefault();
|
||||
const fd = new FormData(e.currentTarget);
|
||||
const r = await fetch('ajax_ajouter_note.php', {method:'POST', body:fd, credentials:'same-origin'});
|
||||
const j = await r.json();
|
||||
if(j.ok){
|
||||
window.location = 'fiche.php?dossier_id=' + encodeURIComponent(<?= (int)$dossier_id ?>);
|
||||
} else {
|
||||
alert(j.message || 'Erreur');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../includes/init_praticien.php';
|
||||
|
||||
$praticien_id = (int)($_SESSION['reference_id'] ?? 0);
|
||||
$file_id = (int)($_GET['id'] ?? 0);
|
||||
if ($praticien_id<=0 || $file_id<=0) { http_response_code(400); exit('Bad request'); }
|
||||
|
||||
// Vérifie la propriété via jointure
|
||||
$sql = "SELECT f.path, f.mime, f.size_bytes
|
||||
FROM client_suivi_files f
|
||||
JOIN client_suivi s ON s.id=f.entry_id
|
||||
JOIN client_dossiers d ON d.id=s.dossier_id
|
||||
WHERE f.id=:id AND d.praticien_id=:p AND d.archived_at IS NULL
|
||||
LIMIT 1";
|
||||
$st = $pdo->prepare($sql);
|
||||
$st->execute([':id'=>$file_id, ':p'=>$praticien_id]);
|
||||
$row = $st->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$row) { http_response_code(403); exit('Forbidden'); }
|
||||
|
||||
$rel = (string)$row['path'];
|
||||
$abs = rtrim((string)$_SERVER['DOCUMENT_ROOT'], '/').$rel;
|
||||
if (!is_file($abs) || !is_readable($abs)) { http_response_code(404); exit('Not found'); }
|
||||
|
||||
$mime = $row['mime'] ?: 'application/octet-stream';
|
||||
$dlName = basename($rel);
|
||||
|
||||
header('Content-Type: '.$mime);
|
||||
header('Content-Length: '.filesize($abs));
|
||||
header('Content-Disposition: attachment; filename="'.rawurlencode($dlName).'"');
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
readfile($abs);
|
||||
@@ -0,0 +1,171 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../includes/init_praticien.php';
|
||||
|
||||
require_once __DIR__ . '/../../includes/header.php';
|
||||
function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
|
||||
|
||||
$praticien_id = (int)($_SESSION['reference_id'] ?? 0);
|
||||
$note_id = (int)($_GET['note_id'] ?? 0);
|
||||
if ($note_id<=0) { header('Location: index.php'); exit; }
|
||||
|
||||
// Vérif propriétaire + récupérer dossier_id pour le retour
|
||||
$sql = "SELECT s.*, d.praticien_id, d.id AS dossier_id
|
||||
FROM client_suivi s
|
||||
JOIN client_dossiers d ON d.id=s.dossier_id
|
||||
WHERE s.id=:id AND d.praticien_id=:p AND d.archived_at IS NULL";
|
||||
$st = $pdo->prepare($sql);
|
||||
$st->execute([':id'=>$note_id, ':p'=>$praticien_id]);
|
||||
$note = $st->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$note) { header('Location: index.php'); exit; }
|
||||
|
||||
$stF = $pdo->prepare("SELECT id, path, mime, size_bytes FROM client_suivi_files WHERE entry_id=:e ORDER BY id ASC");
|
||||
$stF->execute([':e'=>$note_id]);
|
||||
$files = $stF->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (empty($_SESSION['cl_csrf'])) $_SESSION['cl_csrf'] = bin2hex(random_bytes(32));
|
||||
$CSRF = $_SESSION['cl_csrf'];
|
||||
|
||||
?>
|
||||
<div class="main1">
|
||||
<div class="container py-5">
|
||||
<div class="shadow p-4 rounded bg-light">
|
||||
<main class="container my-4">
|
||||
<div class="card p-3 shadow-sm">
|
||||
<h4 class="mb-3"><?= h($translations['modifier'] ?? 'Modifier') ?></h4>
|
||||
|
||||
<form id="edit-form" enctype="multipart/form-data">
|
||||
<input type="hidden" name="csrf" value="<?= h($CSRF) ?>">
|
||||
<input type="hidden" name="note_id" value="<?= (int)$note_id ?>">
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-md-4">
|
||||
<label class="form-label"><?= h($translations['cl_date_rdv'] ?? 'Date') ?></label>
|
||||
<input type="datetime-local" name="date_rdv" class="form-control"
|
||||
value="<?= h((new DateTime($note['date_rdv']))->format('Y-m-d\TH:i')) ?>">
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<label class="form-label"><?= h($translations['cl_type_rdv'] ?? 'Type') ?></label>
|
||||
<select class="form-select" name="type_rdv">
|
||||
<?php foreach([''.$translations['cl_type_online'].'',''.$translations['cl_type_cabinet'].'',''.$translations['cl_type_tel'].'',''.$translations['cl_type_autre'].''] as $opt): ?>
|
||||
<option value="<?= $opt ?>" <?= $note['type_rdv']===$opt?'selected':'' ?>><?= h($opt) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<label class="form-label"><?= h($translations['cl_duree'] ?? 'Durée (min)') ?></label>
|
||||
<input type="number" name="duree_minutes" class="form-control" min="1" max="600" value="<?= h((string)($note['duree_minutes'] ?? '')) ?>">
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label"><?= h($translations['cl_titre'] ?? 'Titre') ?></label>
|
||||
<input type="text" name="titre" class="form-control" required value="<?= h($note['titre']) ?>">
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label"><?= h($translations['cl_notes'] ?? 'Notes') ?></label>
|
||||
<textarea name="notes" class="form-control" rows="6" required><?= h($note['notes']) ?></textarea>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label"><?= h($translations['cl_prochaine_action'] ?? 'Prochaine action') ?></label>
|
||||
<input type="text" name="prochaine_action" class="form-control" value="<?= h((string)$note['prochaine_action']) ?>">
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label"><?= h($translations['cl_pieces_jointes'] ?? 'Pièces jointes') ?></label>
|
||||
<?php if($files): ?>
|
||||
<ul class="list-unstyled">
|
||||
<?php foreach($files as $f): $name = basename((string)$f['path']); ?>
|
||||
<li class="d-flex align-items-center gap-2 mb-1">
|
||||
<span class="small"><?= h($name) ?></span>
|
||||
<a class="btn btn-outline-secondary btn--no-shadow" href="download.php?id=<?= (int)$f['id'] ?>">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M.5 9.9a.5.5 0 0 1 .5.5V13a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V10.4a.5.5 0 0 1 1 0V13a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V10.4a.5.5 0 0 1 .5-.5z"/>
|
||||
<path d="M7.646 10.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 9.293V1.5a.5.5 0 0 0-1 0v7.793L5.354 7.146a.5.5 0 1 0-.708.708l3 3z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<button type="button" class="btn btn-outline-danger btn--no-shadow"
|
||||
data-file="<?= (int)$f['id'] ?>">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M5.5 5.5A.5.5 0 0 1 6 5h4a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-1 0V6h-3v6.5a.5.5 0 0 1-1 0v-7z"/>
|
||||
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1 0-2h3l.5-.5h4l.5.5h3a1 1 0 0 1 1 1zM4.118 4l.82 9.823A1 1 0 0 0 5.933 15h4.134a1 1 0 0 0 .995-.823L11.882 4H4.118z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
<input type="file" name="files[]" class="form-control" multiple accept=".pdf,.jpg,.jpeg,.png,.doc,.docx,.txt">
|
||||
<div class="form-text"><?= $translations['8_max']?></div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 d-flex justify-content-end gap-2">
|
||||
<button class="btn btn-primary" type="submit"><?= h($translations['enregistrer'] ?? 'Enregistrer') ?></button>
|
||||
<a href="fiche.php?dossier_id=<?= (int)$note['dossier_id'] ?>" class="btn btn-outline-secondary"><?= h($translations['cl_pagination_prec'] ?? 'Annuler') ?></a>
|
||||
<button type="button" id="btn-delete-note" class="btn btn-outline-danger btn--no-shadow">
|
||||
<?= h($translations['supprimer'] ?? 'Supprimer') ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p><?= $translations['copyr'] ?></p>
|
||||
</div>
|
||||
</footer>
|
||||
<script>
|
||||
document.getElementById('btn-delete-note')?.addEventListener('click', async ()=>{
|
||||
const res = await Swal.fire({
|
||||
title: <?= json_encode($translations['cl_confirmer_suppression']) ?>,
|
||||
text: <?= json_encode($translations['cl_action_irreversible']) ?>,
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: <?= json_encode($translations['btn_supprimer']) ?>,
|
||||
cancelButtonText: <?= json_encode($translations['btn_annuler']) ?>
|
||||
});
|
||||
if (!res.isConfirmed) return;
|
||||
|
||||
const fd = new FormData();
|
||||
fd.append('csrf', '<?= h($CSRF) ?>');
|
||||
fd.append('note_id', '<?= (int)$note['id'] ?>');
|
||||
|
||||
const r = await fetch('ajax_supprimer_note.php', { method:'POST', body:fd, credentials:'same-origin' });
|
||||
const j = await r.json();
|
||||
if (j.ok) {
|
||||
location.href = 'fiche.php?dossier_id=<?= (int)$note['dossier_id'] ?>';
|
||||
} else {
|
||||
Swal.fire('Oops', j.message || 'Erreur', 'error');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
document.getElementById('edit-form').addEventListener('submit', async (e)=>{
|
||||
e.preventDefault();
|
||||
const fd = new FormData(e.currentTarget);
|
||||
const r = await fetch('ajax_editer_note.php',{method:'POST', body:fd, credentials:'same-origin'});
|
||||
const j = await r.json();
|
||||
if(j.ok){
|
||||
location.href = 'fiche.php?dossier_id=<?= (int)$note['dossier_id'] ?>';
|
||||
}else{
|
||||
alert(j.message||'Erreur');
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', async (e)=>{
|
||||
const b = e.target.closest('button[data-file]');
|
||||
if(!b) return;
|
||||
if(!confirm('Supprimer ce fichier ?')) return;
|
||||
const fd = new FormData();
|
||||
fd.append('csrf', '<?= h($CSRF) ?>');
|
||||
fd.append('file_id', b.getAttribute('data-file'));
|
||||
const r = await fetch('ajax_supprimer_fichier.php',{method:'POST', body:fd, credentials:'same-origin'});
|
||||
const j = await r.json();
|
||||
if(j.ok) location.reload(); else alert(j.message||'Erreur');
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,287 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../includes/init_praticien.php';
|
||||
|
||||
|
||||
function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
|
||||
function humanSize($bytes){
|
||||
$u=['B','KB','MB','GB']; $i=0;
|
||||
while($bytes>=1024 && $i<count($u)-1){ $bytes/=1024; $i++; }
|
||||
return number_format($bytes, $i?1:0, ',', ' ').' '.$u[$i];
|
||||
}
|
||||
function fmtDT(?string $s, string $format='d/m/Y H:i'): string {
|
||||
$s = trim((string)$s);
|
||||
if ($s === '' || $s === '0000-00-00' || $s === '0000-00-00 00:00:00') return '—';
|
||||
try { return (new DateTime($s))->format($format); } catch(Throwable $e){ return '—'; }
|
||||
}
|
||||
|
||||
$praticien_id = (int)($_SESSION['reference_id'] ?? 0);
|
||||
$dossier_id = (int)($_GET['dossier_id'] ?? 0);
|
||||
if ($praticien_id<=0 || $dossier_id<=0) { header('Location: index.php'); exit; }
|
||||
|
||||
/* ----- Dossier + Client (vérif propriétaire) ----- */
|
||||
$st = $pdo->prepare("
|
||||
SELECT
|
||||
d.id, d.praticien_id, d.client_id, d.tags,
|
||||
c.nom, c.prenom, c.email, c.telephone, c.adresse, c.ville, c.code_postal, c.date_naissance
|
||||
FROM client_dossiers d
|
||||
JOIN client c ON c.id = d.client_id
|
||||
WHERE d.id = :d AND d.praticien_id = :p AND d.archived_at IS NULL
|
||||
LIMIT 1
|
||||
");
|
||||
$st->execute([':d'=>$dossier_id, ':p'=>$praticien_id]);
|
||||
$dos = $st->fetch(PDO::FETCH_ASSOC);
|
||||
if(!$dos){ header('Location: index.php'); exit; }
|
||||
|
||||
/* ----- Historique RDV ----- */
|
||||
$stR = $pdo->prepare("
|
||||
SELECT date_rdv, duree_minutes, lieu, statut
|
||||
FROM rendez_vous
|
||||
WHERE praticien_id = :p AND client_id = :c AND (statut = 'terminé' OR statut = 'confirmé')
|
||||
ORDER BY date_rdv DESC
|
||||
LIMIT 100
|
||||
");
|
||||
$stR->execute([':p'=>$praticien_id, ':c'=>$dos['client_id']]);
|
||||
$rdvs = $stR->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
/* ----- Notes + fichiers ----- */
|
||||
$stN = $pdo->prepare("
|
||||
SELECT s.*
|
||||
FROM client_suivi s
|
||||
WHERE s.dossier_id = :d
|
||||
ORDER BY s.date_rdv DESC, s.id DESC
|
||||
");
|
||||
$stN->execute([':d'=>$dossier_id]);
|
||||
$notes = $stN->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$filesByEntry = [];
|
||||
if ($notes) {
|
||||
$ids = array_column($notes, 'id');
|
||||
$ph = implode(',', array_fill(0, count($ids), '?'));
|
||||
$stF = $pdo->prepare("
|
||||
SELECT f.id, f.entry_id, f.path, f.mime, f.size_bytes
|
||||
FROM client_suivi_files f
|
||||
WHERE f.entry_id IN ($ph)
|
||||
ORDER BY f.id ASC
|
||||
");
|
||||
$stF->execute($ids);
|
||||
while($f = $stF->fetch(PDO::FETCH_ASSOC)){
|
||||
$filesByEntry[(int)$f['entry_id']][] = $f;
|
||||
}
|
||||
}
|
||||
|
||||
/* ----- CSRF pour actions (si besoin) ----- */
|
||||
if (empty($_SESSION['cl_csrf'])) $_SESSION['cl_csrf'] = bin2hex(random_bytes(32));
|
||||
$CSRF = $_SESSION['cl_csrf']; ?>
|
||||
<form id="csrf-form" class="d-none">
|
||||
<input type="hidden" name="csrf" value="<?= h($CSRF) ?>">
|
||||
</form>
|
||||
<?php
|
||||
/* Initiales avatar */
|
||||
$initiales = mb_strtoupper(
|
||||
mb_substr((string)$dos['prenom'],0,1,'UTF-8').
|
||||
mb_substr((string)$dos['nom'],0,1,'UTF-8'),
|
||||
'UTF-8'
|
||||
);
|
||||
$fullName = trim(($dos['prenom'] ?? '').' '.($dos['nom'] ?? ''));
|
||||
require_once __DIR__ . '/../../includes/header.php';
|
||||
?>
|
||||
<div class="main1">
|
||||
<div class="container py-5">
|
||||
<div class="shadow p-4 rounded bg-light">
|
||||
<main class="container my-4">
|
||||
<div class="row g-3">
|
||||
<!-- ===== COLONNE GAUCHE : Identité client ===== -->
|
||||
<div class="col-12">
|
||||
<div class="card p-3 shadow-sm">
|
||||
<h4 class="mb-3"><?= h($translations['cl_identite'] ?? 'Identité') ?></h4>
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<!--<div class="avatar"><?= h($initiales) ?></div>-->
|
||||
<div>
|
||||
<div class="fw-bold fs-5"><?= h($fullName) ?></div>
|
||||
<div class="text-muted">
|
||||
<?= h($dos['email']??'') ?><?= !empty($dos['telephone']) ? ' - '.h($dos['telephone']) : '' ?>
|
||||
</div>
|
||||
<?php
|
||||
$addr = trim(($dos['adresse']??'').' '.($dos['code_postal']??'').' '.($dos['ville']??''));
|
||||
if ($addr !== ''):
|
||||
?>
|
||||
<div class="text-muted"><?= h($addr) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($dos['date_naissance'])): ?>
|
||||
<div class="text-muted">
|
||||
<?= h($translations['label_date_naissance'] ?? 'Date de naissance') ?> :
|
||||
<?= fmtDT($dos['date_naissance'], 'd/m/Y') ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($dos['tags'])): ?>
|
||||
<div class="mt-2"><span class="badge bg-light text-dark"><?= h($dos['tags']) ?></span></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 d-flex gap-2">
|
||||
<a class="btn btn-outline-secondary" href="ajouter_note.php?dossier_id=<?= (int)$dossier_id ?>">
|
||||
<?= h($translations['cl_btn_ajouter_note'] ?? 'Ajouter une note') ?>
|
||||
</a>
|
||||
<a class="btn btn-outline-secondary" href="index.php">
|
||||
<?= h($translations['cl_pagination_prec'] ?? 'Retour') ?>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== COLONNE DROITE : RDV + Notes ===== -->
|
||||
<div class="col-12">
|
||||
|
||||
<!-- Notes de suivi -->
|
||||
<div class="card p-3 shadow-sm">
|
||||
<h4 class="mb-3"><?= h($translations['cl_notes_suivi'] ?? 'Notes de suivi') ?></h4>
|
||||
<?php if(!$notes): ?>
|
||||
<div class="text-muted"><?= h($translations['cl_aucun_resultat'] ?? 'Aucun résultat.') ?></div>
|
||||
<?php else: ?>
|
||||
<?php foreach($notes as $n):
|
||||
$files = $filesByEntry[(int)$n['id']] ?? [];
|
||||
?>
|
||||
<div class="mb-3 p-3 border rounded-3">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<div class="fw-bold"><?= h($n['titre']) ?></div>
|
||||
<div class="text-muted small">
|
||||
<?= h(fmtDT($n['date_rdv'])) ?>
|
||||
· <?= h($translations['cl_type_rdv'] ?? 'Type') ?>: <?= h($n['type_rdv']) ?>
|
||||
<?php if(!empty($n['duree_minutes'])): ?>
|
||||
· <?= h($translations['cl_duree'] ?? 'Durée') ?>: <?= (int)$n['duree_minutes'] ?> min
|
||||
<?php endif; ?>
|
||||
<?php if(!empty($n['prochaine_action'])): ?>
|
||||
<br><b><?= h($translations['cl_prochaine_action'] ?? 'Prochaine action') ?>: <?= h($n['prochaine_action']) ?></b>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-2"><?= nl2br(h($n['notes'])) ?></div>
|
||||
|
||||
<?php if ($files): ?>
|
||||
<div class="mt-3">
|
||||
<div class="fw-semibold mb-1">📎 <?= h($translations['pieces_jointes'] ?? 'Pièces jointes') ?></div>
|
||||
<ul class="list-unstyled mb-0">
|
||||
<?php foreach($files as $f):
|
||||
$name = basename((string)$f['path']);
|
||||
?>
|
||||
<li class="d-flex align-items-center gap-2 mb-1">
|
||||
<span class="small"><?= h($name) ?></span>
|
||||
<?php if(!empty($f['size_bytes'])): ?>
|
||||
<span class="text-muted small">· <?= h(humanSize((int)$f['size_bytes'])) ?></span>
|
||||
<?php endif; ?>
|
||||
<a href="download.php?id=<?= (int)$f['id'] ?>" class="btn btn-outline-secondary btn-sm" aria-label="Télécharger">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true" focusable="false">
|
||||
<path d="M.5 9.9a.5.5 0 0 1 .5.5V13a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V10.4a.5.5 0 0 1 1 0V13a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V10.4a.5.5 0 0 1 .5-.5z"/>
|
||||
<path d="M7.646 10.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 9.293V1.5a.5.5 0 0 0-1 0v7.793L5.354 7.146a.5.5 0 1 0-.708.708l3 3z"/>
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<br>
|
||||
<a class="btn btn-outline-secondary"
|
||||
href="editer_note.php?note_id=<?= (int)$n['id'] ?>">
|
||||
<?= h($translations['modifier'] ?? 'Modifier') ?>
|
||||
</a>
|
||||
<!-- Ajout de la classe attendue + data-note existant -->
|
||||
<button type="button"
|
||||
class="btn btn-outline-danger btn--no-shadow btn-del-note"
|
||||
data-note="<?= (int)$n['id'] ?>">
|
||||
<?= h($translations['supprimer'] ?? 'Supprimer') ?>
|
||||
</button>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<!-- Historique des rendez-vous -->
|
||||
<div class="card p-3 shadow-sm mb-3">
|
||||
<h4 class="mb-3"><?= h($translations['cl_histo_rdv'] ?? 'Historique des rendez-vous') ?></h4>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?= h($translations['date_label'] ?? 'Date') ?></th>
|
||||
<th><?= h($translations['cl_type_rdv'] ?? 'Type') ?></th>
|
||||
<th><?= h($translations['cl_duree'] ?? 'Durée') ?></th>
|
||||
<th><?= h($translations['cl_col_tags'] ?? 'Statut') ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if(!$rdvs): ?>
|
||||
<tr><td colspan="4" class="text-muted"><?= h($translations['cl_aucun_resultat'] ?? 'Aucun résultat.') ?></td></tr>
|
||||
<?php else: foreach($rdvs as $r): ?>
|
||||
<tr>
|
||||
<td><?= h(fmtDT($r['date_rdv'])) ?></td>
|
||||
<td><?= h($r['lieu']==='online'
|
||||
? ($translations['cl_type_online']??'En ligne')
|
||||
: ($r['lieu']==='offline' ? ($translations['cl_type_cabinet']??'Au cabinet') : ($translations['cl_type_autre']??'Autre'))) ?></td>
|
||||
<td><?= (int)($r['duree_minutes']??0) ?> min</td>
|
||||
<td><?= h($r['statut']) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p><?= $translations['copyr'] ?></p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sweetalert2@11/dist/sweetalert2.min.css">
|
||||
<script src="/js/sweetalert2@11.js"></script>
|
||||
<script>
|
||||
document.addEventListener('click', async (e)=>{
|
||||
// Plus robuste : on accepte soit .btn-del-note soit tout élément portant data-note
|
||||
const btn = e.target.closest('.btn-del-note, [data-note]');
|
||||
if (!btn) return;
|
||||
|
||||
const noteId = btn.getAttribute('data-note') || '';
|
||||
const res = await Swal.fire({
|
||||
title: <?= json_encode($translations['cl_confirmer_suppression']) ?>,
|
||||
text: <?= json_encode($translations['cl_action_irreversible']) ?>,
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: <?= json_encode($translations['btn_supprimer']) ?>,
|
||||
cancelButtonText: <?= json_encode($translations['btn_annuler']) ?>
|
||||
});
|
||||
if (!res.isConfirmed) return;
|
||||
|
||||
const fd = new FormData(document.getElementById('csrf-form'));
|
||||
fd.append('note_id', noteId);
|
||||
|
||||
try{
|
||||
const r = await fetch('ajax_supprimer_note.php', {
|
||||
method:'POST',
|
||||
body: fd,
|
||||
credentials:'same-origin'
|
||||
});
|
||||
const j = await r.json();
|
||||
if (j.ok) {
|
||||
location.reload();
|
||||
} else {
|
||||
Swal.fire('Oops', j.message || 'Erreur', 'error');
|
||||
}
|
||||
}catch(err){
|
||||
Swal.fire('Oops', 'Erreur réseau', 'error');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,278 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../includes/init_praticien.php';
|
||||
|
||||
|
||||
function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
|
||||
|
||||
$praticien_id = (int)($_SESSION['reference_id'] ?? 0);
|
||||
if ($praticien_id <= 0) { header('Location:/'); exit; }
|
||||
|
||||
/* CSRF */
|
||||
if (empty($_SESSION['cl_csrf'])) $_SESSION['cl_csrf'] = bin2hex(random_bytes(32));
|
||||
$CSRF = $_SESSION['cl_csrf'];
|
||||
|
||||
/* === SSR: première page serveur === */
|
||||
$limit = 10; $offset = 0;
|
||||
|
||||
$sqlTotal = "SELECT COUNT(*) FROM client_dossiers d
|
||||
JOIN client c ON c.id=d.client_id
|
||||
WHERE d.praticien_id=:p AND d.archived_at IS NULL";
|
||||
$stT = $pdo->prepare($sqlTotal);
|
||||
$stT->execute([':p'=>$praticien_id]);
|
||||
$total = (int)$stT->fetchColumn();
|
||||
|
||||
/* ➜ ICI on lit prochaine_action et on l’alias en last_note */
|
||||
$sql = "SELECT d.id AS dossier_id, d.tags,
|
||||
c.nom, c.prenom, c.email, c.telephone,
|
||||
(SELECT MAX(rv.date_rdv) FROM rendez_vous rv
|
||||
WHERE rv.client_id=d.client_id AND rv.praticien_id=d.praticien_id
|
||||
AND rv.statut IN ('confirmé','terminé')) AS last_rdv,
|
||||
(SELECT LEFT(s.prochaine_action, 120) FROM client_suivi s
|
||||
WHERE s.dossier_id=d.id
|
||||
ORDER BY s.date_rdv DESC, s.id DESC LIMIT 1) AS last_note
|
||||
FROM client_dossiers d
|
||||
JOIN client c ON c.id=d.client_id
|
||||
WHERE d.praticien_id=:p AND d.archived_at IS NULL
|
||||
ORDER BY (last_rdv IS NULL) ASC, last_rdv DESC, d.updated_at DESC
|
||||
LIMIT :lim OFFSET :off";
|
||||
$st = $pdo->prepare($sql);
|
||||
$st->bindValue(':p',$praticien_id,PDO::PARAM_INT);
|
||||
$st->bindValue(':lim',$limit,PDO::PARAM_INT);
|
||||
$st->bindValue(':off',$offset,PDO::PARAM_INT);
|
||||
$st->execute();
|
||||
$itemsSSR = $st->fetchAll(PDO::FETCH_ASSOC);
|
||||
$pages = max(1, (int)ceil($total/$limit));
|
||||
require_once __DIR__ . '/../../includes/header.php';
|
||||
?>
|
||||
<div class="main1">
|
||||
<div class="container py-5">
|
||||
<div class="shadow p-4 rounded bg-light">
|
||||
<main class="container my-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h2 class="m-0"><?= h($translations['cl_suivi_titre'] ?? 'Suivi clients') ?></h2>
|
||||
<div>
|
||||
<input id="search" type="search" class="form-control" style="min-width:260px"
|
||||
placeholder="<?= h($translations['cl_recherche_placeholder'] ?? 'Rechercher (nom, email, téléphone, tag)…') ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive mc-ready">
|
||||
<table id="table_rdv" class="table table-striped align-middle text-center">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?= h($translations['cl_col_client'] ?? 'Client') ?></th>
|
||||
<th><?= h($translations['cl_col_dernier_rdv'] ?? 'Dernier RDV') ?></th>
|
||||
<th><?= h($translations['cl_prochaine_action'] ?? 'Prochaine action') ?></th>
|
||||
<th class="text-center"><?= h($translations['cl_actions'] ?? 'Actions') ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="list-body">
|
||||
<?php if (!$itemsSSR): ?>
|
||||
<tr><td colspan="4"><?= h($translations['cl_aucun_resultat'] ?? 'Aucun résultat.') ?></td></tr>
|
||||
<?php else: ?>
|
||||
<?php foreach($itemsSSR as $r):
|
||||
$last = !empty($r['last_rdv']) ? (new DateTime($r['last_rdv']))->format('d/m/Y H:i') : '';
|
||||
$name = trim(($r['prenom'] ?? '').' '.($r['nom'] ?? ''));
|
||||
?>
|
||||
<tr>
|
||||
<td data-label="Client">
|
||||
<div class="fw-bold"><?= h($name) ?></div>
|
||||
</td>
|
||||
<td data-label="Dernier RDV"><?= h($last ?: '—') ?></td>
|
||||
<td data-label="<?= h($translations['cl_col_prochaine_action'] ?? 'Prochaine action') ?>"><?= h($r['last_note'] ?? '') ?></td>
|
||||
<td class="text-center" data-label="<?= h($translations['cl_actions'] ?? 'Actions') ?>">
|
||||
<div class="row-actions d-flex justify-content-center gap-2">
|
||||
<a class="btn btn-outline-secondary" href="fiche.php?dossier_id=<?= (int)$r['dossier_id'] ?>"><?= h($translations['cl_btn_infos'] ?? 'Informations') ?></a>
|
||||
<a class="btn btn-outline-secondary" href="ajouter_note.php?dossier_id=<?= (int)$r['dossier_id'] ?>"><?= h($translations['cl_btn_ajouter_note'] ?? 'Ajouter une note') ?></a>
|
||||
<button type="button"
|
||||
class="btn btn-outline-danger"
|
||||
onclick="deleteDossier(<?= (int)$r['dossier_id'] ?>, this)">
|
||||
<?= h($translations['cl_btn_supprimer'] ?? 'Supprimer') ?>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-center gap-2 my-3" id="pager">
|
||||
<?php for($p=1;$p<=$pages;$p++): ?>
|
||||
<button type="button" class="btn btn-sm <?= $p===1?'btn-secondary':'btn-outline-secondary' ?>" data-page="<?= $p ?>"><?= $p ?></button>
|
||||
<?php endfor; ?>
|
||||
</div>
|
||||
|
||||
<form id="csrf-form" class="d-none"><input type="hidden" name="csrf" value="<?= h($CSRF) ?>"></form>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p><?= $translations['copyr'] ?></p>
|
||||
</div>
|
||||
</footer>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sweetalert2@11/dist/sweetalert2.min.css">
|
||||
<script src="/js/sweetalert2@11.js"></script>
|
||||
|
||||
<?php
|
||||
$I18N = [
|
||||
'confirmer_suppression' => $translations['cl_confirmer_suppression'] ?? 'Confirmer la suppression ?',
|
||||
'action_irreversible' => $translations['cl_action_irreversible'] ?? 'Cette action est irréversible.',
|
||||
'supprimer' => $translations['cl_btn_supprimer'] ?? 'Supprimer',
|
||||
'annuler' => $translations['btn_annuler'] ?? 'Annuler',
|
||||
'aucun_resultat' => $translations['cl_aucun_resultat'] ?? 'Aucun résultat.',
|
||||
'col_prochaine_action' => $translations['cl_col_prochaine_action'] ?? 'Prochaine action',
|
||||
];
|
||||
?>
|
||||
<script>
|
||||
// === i18n (sûr) ===
|
||||
const I18N = <?= json_encode(
|
||||
$I18N,
|
||||
JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT
|
||||
) ?>;
|
||||
function T(k, d){ return (Object.prototype.hasOwnProperty.call(I18N, k) ? I18N[k] : d); }
|
||||
|
||||
// === Utils ===
|
||||
function escapeHtml(s){ return (s||'').replace(/[&<>"']/g, c=>({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[c])); }
|
||||
function fmtDate(s){ return s || '—'; }
|
||||
function debounce(fn,ms){ let t; return (...a)=>{ clearTimeout(t); t=setTimeout(()=>fn(...a),ms); }; }
|
||||
|
||||
// === Rendu dynamique ===
|
||||
function render(items,total,limit){
|
||||
const tbody = document.getElementById('list-body');
|
||||
tbody.innerHTML = '';
|
||||
if(!items || !items.length){
|
||||
const tr = document.createElement('tr');
|
||||
const td = document.createElement('td');
|
||||
td.colSpan = 4;
|
||||
td.textContent = T('aucun_resultat');
|
||||
tr.appendChild(td); tbody.appendChild(tr);
|
||||
} else {
|
||||
items.forEach(it=>{
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td data-label="<?= h($translations['cl_col_client'] ?? 'Client') ?>">
|
||||
<div class="fw-bold">${escapeHtml(it.client_name||'')}</div>
|
||||
</td>
|
||||
<td data-label="<?= h($translations['cl_col_dernier_rdv'] ?? 'Dernier RDV') ?>">${escapeHtml(fmtDate(it.last_rdv_fmt))}</td>
|
||||
<td data-label="${escapeHtml(I18N.col_prochaine_action)}">${escapeHtml(it.last_note||'')}</td>
|
||||
<td class="text-center" data-label="<?= h($translations['cl_actions'] ?? 'Actions') ?>">
|
||||
<div class="row-actions d-flex justify-content-center gap-2">
|
||||
<a class="btn btn-outline-secondary" href="fiche.php?dossier_id=${encodeURIComponent(it.dossier_id)}"><?= h($translations['cl_btn_infos'] ?? 'Informations') ?></a>
|
||||
<a class="btn btn-outline-secondary" href="ajouter_note.php?dossier_id=${encodeURIComponent(it.dossier_id)}"><?= h($translations['cl_btn_ajouter_note'] ?? 'Ajouter une note') ?></a>
|
||||
<button type="button"
|
||||
class="btn btn-outline-danger"
|
||||
onclick="deleteDossier(${Number(''+it.dossier_id)}, this)">
|
||||
<?= h($translations['cl_btn_supprimer'] ?? 'Supprimer') ?>
|
||||
</button>
|
||||
</div>
|
||||
</td>`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
// Pager
|
||||
const pager = document.getElementById('pager');
|
||||
const pages = Math.max(1, Math.ceil(total/limit));
|
||||
pager.innerHTML = '';
|
||||
for(let p=1; p<=pages; p++){
|
||||
const b=document.createElement('button');
|
||||
b.type='button';
|
||||
b.className='btn btn-sm '+(p===window._page?'btn-secondary':'btn-outline-secondary');
|
||||
b.textContent=String(p);
|
||||
b.dataset.page = String(p);
|
||||
b.onclick=()=>{ window._page=p; load(); };
|
||||
pager.appendChild(b);
|
||||
}
|
||||
}
|
||||
|
||||
// === Data ===
|
||||
window._page = 1;
|
||||
window._q = '';
|
||||
|
||||
async function load(){
|
||||
try{
|
||||
const url = `ajax_list.php?q=${encodeURIComponent(window._q)}&page=${window._page}&_=${Date.now()}`;
|
||||
const r = await fetch(url, { credentials:'same-origin', cache:'no-store' });
|
||||
const txt = await r.text();
|
||||
let j;
|
||||
try { j = JSON.parse(txt); }
|
||||
catch(parseErr){ console.error('ajax_list.php non-JSON:', txt); throw new Error('Réponse invalide'); }
|
||||
if(!j.ok) throw new Error(j.message||'Erreur');
|
||||
render(j.items||[], j.total||0, j.limit||10);
|
||||
}catch(e){
|
||||
console.error('load() failed:', e);
|
||||
render([], 0, 10);
|
||||
}
|
||||
}
|
||||
|
||||
// === Recherche ===
|
||||
document.getElementById('search').addEventListener('input', debounce((e)=>{ window._q=e.target.value||''; window._page=1; load(); }, 300));
|
||||
|
||||
// === Suppression : fonction globale appelée par onclick ===
|
||||
window.deleteDossier = async function(dossierId, btnEl){
|
||||
try{
|
||||
const confirmWithSwal = async ()=>{
|
||||
if (typeof Swal !== 'undefined' && Swal.fire) {
|
||||
const res = await Swal.fire({
|
||||
title: T('confirmer_suppression'),
|
||||
text: T('action_irreversible'),
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: T('supprimer'),
|
||||
cancelButtonText: T('annuler')
|
||||
});
|
||||
return res.isConfirmed;
|
||||
}
|
||||
return window.confirm('Supprimer ce dossier ?');
|
||||
};
|
||||
if (!(await confirmWithSwal())) return;
|
||||
|
||||
btnEl?.setAttribute('disabled','disabled');
|
||||
btnEl?.classList.add('disabled');
|
||||
|
||||
const fd = new FormData(document.getElementById('csrf-form'));
|
||||
fd.append('dossier_id', String(dossierId));
|
||||
|
||||
const r = await fetch('ajax_supprimer_dossier.php', {
|
||||
method:'POST', body:fd, credentials:'same-origin', cache:'no-store'
|
||||
});
|
||||
|
||||
let j;
|
||||
try { j = await r.json(); }
|
||||
catch(_){ const txt = await r.text(); console.error('Réponse non-JSON de ajax_supprimer_dossier.php:', txt); alert('Erreur serveur'); return; }
|
||||
|
||||
if (j.ok) {
|
||||
const tr = btnEl?.closest('tr'); if (tr) tr.remove();
|
||||
const rowsLeft = document.querySelectorAll('#list-body > tr').length;
|
||||
if (rowsLeft === 0 && window._page > 1) window._page--;
|
||||
await load();
|
||||
if (typeof Swal !== 'undefined' && Swal.fire) {
|
||||
Swal.fire({ icon:'success', title:'OK', text:'Dossier supprimé', timer:1200, showConfirmButton:false });
|
||||
}
|
||||
} else {
|
||||
alert(j.message || 'Erreur');
|
||||
}
|
||||
}catch(err){
|
||||
console.error(err);
|
||||
alert('Erreur réseau');
|
||||
}finally{
|
||||
btnEl?.removeAttribute('disabled');
|
||||
btnEl?.classList.remove('disabled');
|
||||
}
|
||||
};
|
||||
|
||||
// === Pager SSR (premier rendu) ===
|
||||
document.getElementById('pager').addEventListener('click', (e)=>{
|
||||
const b = e.target.closest('button[data-page]');
|
||||
if(!b) return;
|
||||
window._page = parseInt(b.dataset.page || '1', 10) || 1;
|
||||
load();
|
||||
});
|
||||
|
||||
// === Initial load ===
|
||||
load();
|
||||
</script>
|
||||
Reference in new issue
Block a user