mirror of
https://github.com/JMP-MS/ASPIC.git
synced 2026-08-28 23:33:13 +00:00
302 lines
14 KiB
PHP
302 lines
14 KiB
PHP
<?php
|
|
/**
|
|
* ASPIC — contacts.php
|
|
* API CRUD sur ASP_Contacts (GET liste + fiche / POST création / PUT
|
|
* modification / DELETE suppression logique).
|
|
*
|
|
* ⚠️ Reconstruction du 24 août 2026, à comparer avec le fichier actuellement
|
|
* en production avant remplacement. Cette session n'avait pas accès au
|
|
* contenu exact de la version en ligne (alignée le 21 août 2026, avec les
|
|
* correctifs du 23 août sur le joker « * » et sur PUT/DELETE — voir journal
|
|
* de développement) ; ce fichier reprend fidèlement tout ce qui est
|
|
* documenté dans le journal, mais mérite une comparaison avant déploiement
|
|
* si la version en ligne contient des ajustements non journalisés.
|
|
*
|
|
* Reprend :
|
|
* - GET liste : tri restreint à une liste blanche de colonnes, jusqu'à 7
|
|
* filtres optionnels avec joker "*", WHERE CNT_Actif = 1 systématique.
|
|
* - GET fiche (?id=) : un contact actif.
|
|
* - POST : création. Génère CNT_Code (25 août 2026, voir journal section 16) :
|
|
* 1. Recherche ANN_Code dans ASP_Annees pour l'année courante — AVANT
|
|
* l'INSERT, pour ne jamais créer un contact sans code si l'année n'est
|
|
* pas encore configurée (échec net, 500, plutôt qu'un contact orphelin
|
|
* de code à corriger après coup).
|
|
* 2. INSERT du contact, lastInsertId() = CNT_Incr.
|
|
* 3. Nombre aléatoire 0-999, zéro-padé sur 3 caractères.
|
|
* 4. UPDATE CNT_Code = ANN_Code . "-" . CNT_Incr (sans zéro de tête) .
|
|
* nombre aléatoire (3 caractères).
|
|
* La journalisation ASP_Realisations (CNT_CRE) reste à la charge du
|
|
* front-end, sur le même principe que CNT_MAJ pour la modification (POST
|
|
* séparé vers realisations.php, best-effort, ne bloque pas la création).
|
|
* - PUT (?id=) : modification, sur l'ensemble des champs éditables du
|
|
* panneau de détail (étendu le 24 août 2026 — l'ancien prototype ne
|
|
* couvrait que Nom/Prénom/Mail).
|
|
* - DELETE (?id=) : suppression logique (CNT_Actif = 0), jamais de DELETE
|
|
* SQL réel.
|
|
* - PUT vérifie l'EXISTENCE du contact par un SELECT séparé de l'UPDATE lui
|
|
* même (corrigé le 24 août 2026 — voir note dans le bloc PUT ci-dessous).
|
|
* DELETE continue de s'appuyer sur rowCount(), valide dans son cas.
|
|
* - Toute exception PDO (contenu qui ne passe pas une contrainte de colonne,
|
|
* erreur de connexion perdue en cours de route, etc.) est interceptée et
|
|
* renvoyée en JSON avec un code 500, plutôt que de laisser remonter une
|
|
* erreur PHP brute (non-JSON) que le front-end ne peut pas interpréter
|
|
* proprement — correctif du 24 août 2026, voir journal.
|
|
*
|
|
* Note sur le joker "*" : contrairement à la première version (bug corrigé
|
|
* le 23 août 2026), l'échappement du LIKE n'est JAMAIS construit par
|
|
* concaténation dans le texte SQL. Le caractère d'échappement est passé en
|
|
* paramètre lié (ESCAPE ?), comme le motif lui-même — ce qui évite par
|
|
* construction le piège du antislash interprété différemment dans un
|
|
* littéral SQL selon le moteur (MySQL/MariaDB vs SQLite, voir journal).
|
|
*
|
|
* ⚠️ Session utilisateur intégrée le 24 août 2026 (voir journal, sections 14
|
|
* et 15) : tout appel exige désormais une session PHP valide, ouverte par
|
|
* `aspic.php` à l'identification (`$_SESSION['USR_Code']`). Sans elle,
|
|
* l'endpoint répond 401 — y compris en lecture (GET). C'est `aspic.php` qui
|
|
* doit appeler `session_start()` et renseigner `$_SESSION['USR_Code']` au
|
|
* moment où l'identifiant/mot de passe sont validés ; sans cette mise à jour
|
|
* côté `aspic.php`, plus aucun appel à ce fichier ne pourra aboutir.
|
|
*/
|
|
|
|
session_start();
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
require __DIR__ . '/db.php';
|
|
|
|
// Accès réservé aux utilisateurs identifiés (session ouverte par aspic.php).
|
|
if (empty($_SESSION['USR_Code'])) {
|
|
http_response_code(401);
|
|
echo json_encode(['error' => 'Non identifié — veuillez vous connecter.']);
|
|
exit;
|
|
}
|
|
|
|
$method = $_SERVER['REQUEST_METHOD'];
|
|
|
|
try {
|
|
$pdo = getDbConnection();
|
|
} catch (PDOException $e) {
|
|
http_response_code(500);
|
|
echo json_encode(['error' => 'Connexion base de données impossible']);
|
|
exit;
|
|
}
|
|
|
|
// Colonnes éditables depuis le panneau de détail du contact (hors CNT_Code,
|
|
// toujours généré/lecture seule, et hors CNT_Incr/CNT_Actif).
|
|
const CHAMPS_EDITABLES = [
|
|
'CNT_Nom', 'CNT_Prenom', 'CNT_Statut', 'CNT_Société', 'CNT_Service',
|
|
'CNT_Fonction', 'CNT_AdrLig1', 'CNT_AdrLig2', 'CNT_AdrLig3', 'CNT_AdrCP',
|
|
'CNT_AdrVille', 'CNT_SIRET', 'CNT_Tel', 'CNT_Portable', 'CNT_Mail',
|
|
'CNT_Notes', 'CNT_CodeProfession',
|
|
];
|
|
|
|
// Liste blanche des colonnes de tri autorisées (celles réellement triables
|
|
// depuis l'écran liste des contacts).
|
|
const COLONNES_TRI_AUTORISEES = ['CNT_Nom', 'CNT_AdrVille'];
|
|
|
|
/**
|
|
* Construit un motif LIKE à partir d'une valeur de filtre utilisateur,
|
|
* en échappant d'abord les caractères spéciaux LIKE déjà présents
|
|
* (antislash, %, _) avant de traduire le joker "*" de l'utilisateur en "%".
|
|
* Le caractère d'échappement utilisé (un seul antislash) est renvoyé à part,
|
|
* pour être passé en paramètre lié à la clause ESCAPE — jamais concaténé
|
|
* dans le texte SQL (voir note d'en-tête).
|
|
*/
|
|
function construireMotifLike(string $valeur): array
|
|
{
|
|
$echappe = str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], $valeur);
|
|
$motif = str_replace('*', '%', $echappe);
|
|
return [$motif, '\\'];
|
|
}
|
|
|
|
try {
|
|
switch ($method) {
|
|
case 'GET':
|
|
if (isset($_GET['id'])) {
|
|
$stmt = $pdo->prepare('SELECT * FROM ASP_Contacts WHERE CNT_Incr = ? AND CNT_Actif = 1');
|
|
$stmt->execute([$_GET['id']]);
|
|
$contact = $stmt->fetch();
|
|
if (!$contact) {
|
|
http_response_code(404);
|
|
echo json_encode(['error' => 'Contact introuvable']);
|
|
exit;
|
|
}
|
|
echo json_encode($contact);
|
|
exit;
|
|
}
|
|
|
|
$tri = $_GET['tri'] ?? 'CNT_Nom';
|
|
if (!in_array($tri, COLONNES_TRI_AUTORISEES, true)) {
|
|
$tri = 'CNT_Nom';
|
|
}
|
|
$sens = (isset($_GET['sens']) && strtolower($_GET['sens']) === 'desc') ? 'DESC' : 'ASC';
|
|
|
|
$conditions = ['CNT_Actif = 1'];
|
|
$params = [];
|
|
|
|
$filtresColonnes = [
|
|
'f_nom' => 'CNT_Nom',
|
|
'f_prenom' => 'CNT_Prenom',
|
|
'f_ville' => 'CNT_AdrVille',
|
|
'f_tel' => 'CNT_Tel',
|
|
'f_portable' => 'CNT_Portable',
|
|
'f_mail' => 'CNT_Mail',
|
|
];
|
|
foreach ($filtresColonnes as $param => $colonne) {
|
|
if (!empty($_GET[$param])) {
|
|
[$motif, $echap] = construireMotifLike($_GET[$param]);
|
|
$conditions[] = "$colonne LIKE ? ESCAPE ?";
|
|
$params[] = $motif;
|
|
$params[] = $echap;
|
|
}
|
|
}
|
|
// Département = 2 premiers caractères de CNT_AdrCP.
|
|
if (!empty($_GET['f_dept'])) {
|
|
[$motif, $echap] = construireMotifLike($_GET['f_dept']);
|
|
$conditions[] = 'LEFT(CNT_AdrCP, 2) LIKE ? ESCAPE ?';
|
|
$params[] = $motif;
|
|
$params[] = $echap;
|
|
}
|
|
|
|
$sql = 'SELECT *, LEFT(CNT_AdrCP, 2) AS CNT_Dept FROM ASP_Contacts WHERE '
|
|
. implode(' AND ', $conditions)
|
|
. " ORDER BY $tri $sens, CNT_Prenom ASC";
|
|
|
|
$stmtTotal = $pdo->prepare('SELECT COUNT(*) FROM ASP_Contacts WHERE ' . implode(' AND ', $conditions));
|
|
$stmtTotal->execute($params);
|
|
$total = (int)$stmtTotal->fetchColumn();
|
|
|
|
$stmt = $pdo->prepare($sql);
|
|
$stmt->execute($params);
|
|
echo json_encode(['contacts' => $stmt->fetchAll(), 'total' => $total]);
|
|
break;
|
|
|
|
case 'POST':
|
|
$data = json_decode(file_get_contents('php://input'), true) ?? [];
|
|
$nom = trim($data['CNT_Nom'] ?? '');
|
|
if ($nom === '') {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Le nom est obligatoire']);
|
|
exit;
|
|
}
|
|
|
|
// Étape 1 (voir en-tête du fichier) : ANN_Code de l'année courante,
|
|
// recherché AVANT l'INSERT pour ne jamais créer un contact sans
|
|
// code faute d'année configurée dans ASP_Annees.
|
|
$anneeCourante = (int)date('Y');
|
|
$stmtAnnee = $pdo->prepare('SELECT ANN_Code FROM ASP_Annees WHERE ANN_Année = ?');
|
|
$stmtAnnee->execute([$anneeCourante]);
|
|
$annCode = $stmtAnnee->fetchColumn();
|
|
if ($annCode === false) {
|
|
http_response_code(500);
|
|
echo json_encode(['error' => "Aucun code configuré pour l'année $anneeCourante dans ASP_Annees — impossible de générer le Code du contact."]);
|
|
exit;
|
|
}
|
|
|
|
$colonnes = [];
|
|
$placeholders = [];
|
|
$params = [];
|
|
foreach (CHAMPS_EDITABLES as $champ) {
|
|
$colonnes[] = $champ;
|
|
$placeholders[] = '?';
|
|
$valeur = $data[$champ] ?? null;
|
|
$params[] = ($valeur === '' ? null : $valeur);
|
|
}
|
|
|
|
// Étape 2 : création du contact (CNT_Code encore NULL à ce stade,
|
|
// il dépend de CNT_Incr qui n'existe qu'après l'INSERT).
|
|
$sql = 'INSERT INTO ASP_Contacts (' . implode(', ', $colonnes) . ') VALUES (' . implode(', ', $placeholders) . ')';
|
|
$stmt = $pdo->prepare($sql);
|
|
$stmt->execute($params);
|
|
$nouvelIncr = (int)$pdo->lastInsertId();
|
|
|
|
// Étapes 3-4 : nombre aléatoire 0-999 zéro-padé sur 3 caractères,
|
|
// concaténation ANN_Code-CNT_Incr+aléatoire (voir journal section 2
|
|
// pour la logique détaillée, et section 16 pour cette implémentation).
|
|
$aleatoire = str_pad((string)random_int(0, 999), 3, '0', STR_PAD_LEFT);
|
|
$code = $annCode . '-' . $nouvelIncr . $aleatoire;
|
|
|
|
$stmtCode = $pdo->prepare('UPDATE ASP_Contacts SET CNT_Code = ? WHERE CNT_Incr = ?');
|
|
$stmtCode->execute([$code, $nouvelIncr]);
|
|
|
|
echo json_encode(['id' => $nouvelIncr, 'code' => $code]);
|
|
break;
|
|
|
|
case 'PUT':
|
|
if (!isset($_GET['id'])) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'id manquant']);
|
|
exit;
|
|
}
|
|
$data = json_decode(file_get_contents('php://input'), true) ?? [];
|
|
$nom = trim($data['CNT_Nom'] ?? '');
|
|
if ($nom === '') {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Le nom est obligatoire']);
|
|
exit;
|
|
}
|
|
|
|
// Vérification d'EXISTENCE séparée de l'UPDATE lui-même : par
|
|
// défaut, rowCount() après un UPDATE reflète le nombre de lignes
|
|
// RÉELLEMENT MODIFIÉES par MySQL/MariaDB, pas le nombre de lignes
|
|
// trouvées par le WHERE. Une re-soumission du formulaire sans
|
|
// changement réel (ou un enregistrement où, par coïncidence,
|
|
// aucune des valeurs soumises ne diffère de ce qui est déjà en
|
|
// base) donnerait alors rowCount() = 0, et l'ancien code
|
|
// l'interprétait à tort comme "contact introuvable" (404) — bug
|
|
// corrigé le 24 août 2026, repéré par Jean-Marc ("erreur si je
|
|
// remplis Notes ou Informations", en réalité lié à des essais
|
|
// répétés sans changement effectif des champs du contact).
|
|
$verif = $pdo->prepare('SELECT CNT_Incr FROM ASP_Contacts WHERE CNT_Incr = ? AND CNT_Actif = 1');
|
|
$verif->execute([$_GET['id']]);
|
|
if (!$verif->fetch()) {
|
|
http_response_code(404);
|
|
echo json_encode(['error' => 'Contact introuvable ou déjà désactivé']);
|
|
exit;
|
|
}
|
|
|
|
$assignations = [];
|
|
$params = [];
|
|
foreach (CHAMPS_EDITABLES as $champ) {
|
|
$assignations[] = "$champ = ?";
|
|
$valeur = $data[$champ] ?? null;
|
|
$params[] = ($valeur === '' ? null : $valeur);
|
|
}
|
|
$params[] = $_GET['id'];
|
|
|
|
$sql = 'UPDATE ASP_Contacts SET ' . implode(', ', $assignations) . ' WHERE CNT_Incr = ? AND CNT_Actif = 1';
|
|
$stmt = $pdo->prepare($sql);
|
|
$stmt->execute($params);
|
|
|
|
echo json_encode(['success' => true]);
|
|
break;
|
|
|
|
case 'DELETE':
|
|
if (!isset($_GET['id'])) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'id manquant']);
|
|
exit;
|
|
}
|
|
// Suppression logique uniquement (voir journal, section 2) —
|
|
// jamais de DELETE SQL réel. Ici rowCount() reste un test valide
|
|
// (contrairement au PUT ci-dessus) : CNT_Actif passe toujours
|
|
// réellement de 1 à 0, donc rowCount() = 0 signifie bien "déjà
|
|
// désactivé ou inexistant".
|
|
$stmt = $pdo->prepare('UPDATE ASP_Contacts SET CNT_Actif = 0 WHERE CNT_Incr = ? AND CNT_Actif = 1');
|
|
$stmt->execute([$_GET['id']]);
|
|
|
|
if ($stmt->rowCount() === 0) {
|
|
http_response_code(404);
|
|
echo json_encode(['error' => 'Contact introuvable ou déjà désactivé']);
|
|
exit;
|
|
}
|
|
echo json_encode(['success' => true]);
|
|
break;
|
|
|
|
default:
|
|
http_response_code(405);
|
|
echo json_encode(['error' => 'Méthode non autorisée']);
|
|
}
|
|
} catch (PDOException $e) {
|
|
http_response_code(500);
|
|
echo json_encode(['error' => 'Erreur base de données : ' . $e->getMessage()]);
|
|
}
|