mirror of
https://github.com/JMP-MS/ASPIC.git
synced 2026-10-27 16:07:10 +00:00
406 lines
20 KiB
PHP
406 lines
20 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.
|
|
*
|
|
* ⚙️ Chantier des consignes (6 septembre 2026, voir Réflexions stratégiques) :
|
|
* POST et PUT n'ont plus aucun contrôle ni traitement écrit en dur sur les
|
|
* champs — un seul appel à appliquerConsignes() (moteur_consignes.php), qui
|
|
* lit ASP_Consignes/ASP_Fonctions à chaque appel et applique ce qu'il y
|
|
* trouve pour le contexte 'Contacts' (portée 'Création' ou 'Modification'
|
|
* selon le cas). Ajouter/modifier une consigne en base change donc le
|
|
* comportement réel sans toucher ce fichier — sauf si une fonction nécessaire
|
|
* n'existe pas encore dans consignes_fonctions.php, ou en cas de bug.
|
|
* Version volontairement simplifiée (voir en-tête de moteur_consignes.php) :
|
|
* pas de distinction Saisie/Validation (tout s'applique à l'enregistrement),
|
|
* pas de vérification en temps réel côté écran pour l'instant — décision de
|
|
* Jean-Marc du 6 septembre 2026, priorité à la robustesse.
|
|
*
|
|
* Journalisation des Traitements IA dans ASP_Realisations (7 septembre 2026,
|
|
* soir) : appliquerConsignes() peut remonter, via son paramètre par référence
|
|
* $journalisationsIA, une ou plusieurs entrées à journaliser (ex. code postal
|
|
* déduit par l'IA depuis la ville — voir consigne_completer_code_postal()).
|
|
* Écrites ICI, directement en base, plutôt que laissées au front-end comme
|
|
* CNT_CRE/CNT_MAJ (voir liste_contacts.html, validerFormulaireContact()) :
|
|
* REA_CodeUser/REA_NomAffiche reçoivent alors le nom d'agent IA (ex. "Matilda"
|
|
* — voir ASP_Agents.AGN_Prenom, et CNS_Agent sur la consigne qui a déclenché le
|
|
* Traitement), pas
|
|
* l'utilisateur humain de la session — hors de portée de realisations.php,
|
|
* qui impose volontairement $_SESSION['USR_Code'] (correctif de sécurité du
|
|
* 24 août 2026, voir ce fichier) et ne doit surtout pas rouvrir la
|
|
* possibilité qu'un client falsifie REA_CodeUser. Best-effort (try/catch
|
|
* séparé) : un souci de journalisation ne doit jamais faire échouer
|
|
* l'enregistrement du contact lui-même, même principe que la journalisation
|
|
* CNT_CRE/CNT_MAJ côté front.
|
|
*/
|
|
|
|
session_start();
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
require __DIR__ . '/db.php';
|
|
require __DIR__ . '/consignes_fonctions.php';
|
|
require __DIR__ . '/moteur_consignes.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, '\\'];
|
|
}
|
|
|
|
/**
|
|
* Journalise dans ASP_Realisations les Traitements IA remontés par
|
|
* appliquerConsignes() (voir moteur_consignes.php et le commentaire d'en-tête
|
|
* de ce fichier) — ex. code postal déduit de la ville. Best-effort : une
|
|
* exception ici est avalée (juste consignée au log serveur), jamais renvoyée
|
|
* au client — un souci de journalisation ne doit pas laisser croire que
|
|
* l'enregistrement du contact a échoué alors qu'il a réussi.
|
|
*/
|
|
function journaliserRealisationsIA(PDO $pdo, int $idContact, array $journalisationsIA): void
|
|
{
|
|
if (!$journalisationsIA) return;
|
|
try {
|
|
// REA_CodeUser ET REA_NomAffiche reçoivent la même valeur pour un
|
|
// agent IA (7 septembre 2026, nuit — "le prénom de l'agent en dur",
|
|
// Jean-Marc) : contrairement à un utilisateur humain, un agent n'a
|
|
// pas de code technique court séparé de son nom affiché — la valeur
|
|
// vient déjà en clair de CNS_Agent (voir moteur_consignes.php),
|
|
// aucune recherche supplémentaire à faire ici.
|
|
$stmt = $pdo->prepare(
|
|
'INSERT INTO ASP_Realisations (REA_IncrContact, REA_CodeUser, REA_NomAffiche, REA_Date, REA_Action, REA_Comment)
|
|
VALUES (?, ?, ?, NOW(), ?, ?)'
|
|
);
|
|
foreach ($journalisationsIA as $j) {
|
|
$nomAgent = (string)($j['agent'] ?? 'IA');
|
|
$stmt->execute([
|
|
$idContact,
|
|
$nomAgent,
|
|
$nomAgent,
|
|
(string)($j['action'] ?? 'IA'),
|
|
(string)($j['commentaire'] ?? ''),
|
|
]);
|
|
}
|
|
} catch (PDOException $e) {
|
|
error_log('ASPIC contacts: échec journalisation ASP_Realisations (Traitement IA) : ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// Comptage seul (?compte=1), ajouté le 2 septembre 2026 pour la
|
|
// pastille du classeur "Contacts" sur l'écran de fond (voir
|
|
// journal section 28) : le nombre de contacts actifs, sans
|
|
// charger la liste complète juste pour ça. Volontairement
|
|
// indépendant des filtres f_* (le classeur affiche le total réel
|
|
// de la table, pas le résultat d'une recherche en cours).
|
|
if (isset($_GET['compte'])) {
|
|
$total = (int)$pdo->query('SELECT COUNT(*) FROM ASP_Contacts WHERE CNT_Actif = 1')->fetchColumn();
|
|
echo json_encode(['total' => $total]);
|
|
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) ?? [];
|
|
|
|
$erreurConsigne = appliquerConsignes($pdo, 'Contacts', 'Création', $data, $agentConsigne, $journalisationsIA, $avertissementsIA);
|
|
if ($erreurConsigne !== null) {
|
|
http_response_code(400);
|
|
// 'agent' (CNS_Agent, 7 septembre 2026) : vaut JSON null si la
|
|
// consigne en échec n'en a pas — sans effet côté front, où
|
|
// afficherToast n'affiche la ligne agent que si la valeur est
|
|
// présente et non vide.
|
|
echo json_encode(['error' => $erreurConsigne, 'agent' => $agentConsigne]);
|
|
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]);
|
|
|
|
journaliserRealisationsIA($pdo, $nouvelIncr, $journalisationsIA);
|
|
|
|
// 'avertissements' (7 septembre 2026, nuit) : messages à afficher
|
|
// immédiatement à l'écran, voir moteur_consignes.php ("_avertissement")
|
|
// — distinct de la journalisation ci-dessus, qui va dans
|
|
// l'historique mais ne s'affiche pas tout de suite. Tableau vide
|
|
// la plupart du temps, le front-end n'affiche alors rien.
|
|
echo json_encode(['id' => $nouvelIncr, 'code' => $code, 'avertissements' => $avertissementsIA]);
|
|
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) ?? [];
|
|
|
|
// Dernier paramètre (8 septembre 2026) : identifiant du contact
|
|
// en cours de modification, transmis pour qu'une consigne
|
|
// d'unicité (ex. mail/portable déjà utilisé, voir
|
|
// consignes_fonctions.php) puisse exclure cette fiche elle-même
|
|
// de sa recherche — sans quoi ré-enregistrer un contact sans
|
|
// changer son mail se bloquerait lui-même. Absent en POST
|
|
// ci-dessus : en Création, aucun contact n'existe encore à
|
|
// exclure.
|
|
$erreurConsigne = appliquerConsignes($pdo, 'Contacts', 'Modification', $data, $agentConsigne, $journalisationsIA, $avertissementsIA, (int)$_GET['id']);
|
|
if ($erreurConsigne !== null) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => $erreurConsigne, 'agent' => $agentConsigne]);
|
|
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);
|
|
|
|
journaliserRealisationsIA($pdo, (int)$_GET['id'], $journalisationsIA);
|
|
|
|
echo json_encode(['success' => true, 'avertissements' => $avertissementsIA]);
|
|
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()]);
|
|
}
|