'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()]); }