--- /dev/null
+<?php
+/* =======================================================
+ Super-DB One-File avec pagination SQL et multi-colonnes
+ Layout corrigé : [Lang] seule ligne, [Recherche][Pagination] sur ligne suivante
+ ======================================================= */
+class TableFilter
+{
+ private PDO $pdo;
+ private string $table;
+ private array $columns;
+
+ public function __construct(PDO $pdo, string $table, array $columns)
+ {
+ $this->pdo = $pdo;
+ $this->table = $table;
+ $this->columns = $columns;
+ }
+
+ public function fetchFiltered(array $selectedColumns, string $search, int $limit): array
+ {
+ if(empty($selectedColumns) || trim($search)===''){
+ $sql = "SELECT * FROM {$this->table} ORDER BY creation DESC LIMIT :limit";
+ $stmt = $this->pdo->prepare($sql);
+ $stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
+ $stmt->execute();
+ return $stmt->fetchAll(PDO::FETCH_ASSOC);
+ }
+
+ $whereParts = [];
+ $params = [];
+ foreach($selectedColumns as $i => $col){
+ if(!in_array($col,$this->columns,true)) continue;
+ $whereParts[] = "$col ILIKE :s$i";
+ $params[":s$i"] = "%$search%";
+ }
+ $where = implode(" OR ", $whereParts);
+ $sql = "SELECT * FROM {$this->table} WHERE $where ORDER BY creation DESC LIMIT :limit";
+ $stmt = $this->pdo->prepare($sql);
+ foreach($params as $k=>$v) $stmt->bindValue($k,$v);
+ $stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
+ $stmt->execute();
+ return $stmt->fetchAll(PDO::FETCH_ASSOC);
+ }
+}
+
+/* =======================================================
+ Gestion AJAX
+ ======================================================= */
+if(php_sapi_name()!=='cli' && isset($_POST['search'])){
+ header("Content-Type: application/json");
+ $pdo = new PDO("pgsql:host=localhost;dbname=mailleur","mailleur","mailleurpass",
+ [PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION]);
+ $filter = new TableFilter($pdo,"actions",["remoteip","reverse","creation"]);
+ $selected = $_POST["columns"]??[];
+ $search = $_POST["search"]??"";
+ $limit = (int)($_POST["limit"]??20);
+ $data = $filter->fetchFiltered($selected,$search,$limit);
+ echo json_encode($data);
+ exit;
+}
+
+/* =======================================================
+ Page HTML
+ ======================================================= */
+$lang = $_GET["lang"]??"en";
+$labels = ["en"=>["Remote IP","Reverse","Creation"],"fr"=>["IP distante","Reverse","Création"]];
+$colKeys = ["remoteip","reverse","creation"];
+$L = $labels[$lang];
+?>
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Bigre Super-DB Paginated JMP</title>
+<style>
+.table-filter-row { display:flex; gap:8px; align-items:center; margin-bottom:6px; flex-wrap: wrap; }
+#langSelect-row { display:block; margin-bottom:12px; } /* isole Lang sur sa propre ligne */
+table{border-collapse:collapse;width:100%;}
+th,td{padding:6px;border:1px solid #888;}
+.filter-col{cursor:pointer;color:#003;background:#f7f7ff;}
+.filter-col.active{background:#cce;}
+</style>
+</head>
+<body>
+
+<!-- Ligne 1 : Langue -->
+<div id="langSelect-row">
+ <select id="langSelect" onchange="window.location='?lang='+this.value;">
+ <option value="en" <?= $lang==="en"?"selected":"" ?>>English</option>
+ <option value="fr" <?= $lang==="fr"?"selected":"" ?>>Français</option>
+ </select>
+</div>
+
+<!-- Ligne 2 : Recherche + Pagination -->
+<div class="table-filter-row">
+ <input type="text" id="searchInput" placeholder="Type to filter…">
+ <select id="limitSelect">
+ <?php foreach([5,10,20,40,80] as $n): ?>
+ <option value="<?= $n ?>" <?= $n===20?"selected":"" ?>><?= $n ?></option>
+ <?php endforeach; ?>
+ </select>
+</div>
+
+<!-- Tableau -->
+<table id="dataTable">
+ <thead>
+ <tr>
+ <?php foreach($L as $i=>$label): ?>
+ <th class="filter-col" data-col="<?= $colKeys[$i] ?>"><?= htmlspecialchars($label) ?></th>
+ <?php endforeach; ?>
+ </tr>
+ </thead>
+ <tbody></tbody>
+</table>
+
+<script>
+let columns=[];
+const searchInput = document.getElementById("searchInput");
+const limitSelect = document.getElementById("limitSelect");
+
+// Header click pour multi-colonnes
+document.querySelectorAll(".filter-col").forEach(th=>{
+ th.addEventListener("click",()=>{
+ const col = th.dataset.col;
+ if(columns.includes(col)){
+ columns = columns.filter(c=>c!==col);
+ th.classList.remove("active");
+ } else {
+ columns.push(col);
+ th.classList.add("active");
+ }
+ refresh();
+ });
+});
+
+// Recherche et changement de limit
+searchInput.addEventListener("input",refresh);
+limitSelect.addEventListener("change",refresh);
+
+// AJAX vers le même fichier
+function refresh(){
+ const form = new FormData();
+ form.append("search", searchInput.value);
+ form.append("limit", limitSelect.value);
+ columns.forEach(c=>form.append("columns[]",c));
+
+ fetch("", {method:"POST", body:form})
+ .then(r=>r.json())
+ .then(json=>renderTable(json));
+}
+
+// Remplissage tableau
+function renderTable(rows){
+ const tbody=document.querySelector("#dataTable tbody");
+ tbody.innerHTML="";
+ for(const row of rows){
+ const tr=document.createElement("tr");
+ tr.innerHTML=`<td>${row.remoteip}</td><td>${row.reverse}</td><td>${row.creation}</td>`;
+ tbody.appendChild(tr);
+ }
+}
+
+// Load initial
+refresh();
+</script>
+
+</body>
+</html>
+
--- /dev/null
+<?php
+/* =======================================================
+ Super-DB One-File corrigé : affichage NULL explicite
+ ======================================================= */
+class TableFilter
+{
+ private PDO $pdo;
+ private string $table;
+ private array $columns;
+
+ public function __construct(PDO $pdo, string $table, array $columns)
+ {
+ $this->pdo = $pdo;
+ $this->table = $table;
+ $this->columns = $columns;
+ }
+
+ public function fetchFiltered(array $selectedColumns, string $search, int $limit): array
+ {
+ if(empty($selectedColumns)) $selectedColumns = $this->columns;
+
+ if(trim($search)===''){
+ $sql = "SELECT * FROM {$this->table} ORDER BY creation DESC LIMIT :limit";
+ $stmt = $this->pdo->prepare($sql);
+ $stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
+ $stmt->execute();
+ return $stmt->fetchAll(PDO::FETCH_ASSOC);
+ }
+
+ $whereParts = [];
+ $params = [];
+ foreach($selectedColumns as $i => $col){
+ if(!in_array($col,$this->columns,true)) continue;
+ $key = ":s$i";
+ $whereParts[] = "$col ILIKE $key";
+ $params[$key] = "%$search%";
+ }
+
+ $where = implode(" OR ", $whereParts);
+ $sql = "SELECT * FROM {$this->table} WHERE $where ORDER BY creation DESC LIMIT :limit";
+ $stmt = $this->pdo->prepare($sql);
+
+ foreach($params as $k=>$v){
+ $stmt->bindValue($k,$v,PDO::PARAM_STR);
+ }
+ $stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
+ $stmt->execute();
+ return $stmt->fetchAll(PDO::FETCH_ASSOC);
+ }
+}
+
+/* =======================================================
+ Gestion AJAX
+ ======================================================= */
+if(php_sapi_name()!=='cli' && isset($_POST['search'])){
+ header("Content-Type: application/json");
+ $pdo = new PDO("pgsql:host=localhost;dbname=mailleur","mailleur","mailleurpass",
+ [PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION]);
+ $filter = new TableFilter($pdo,"actions",["remoteip","reverse","creation"]);
+
+ $selected = $_POST["columns"]??[];
+ if(empty($selected)) $selected = ["remoteip","reverse","creation"];
+
+ $search = $_POST["search"]??"";
+ $limit = (int)($_POST["limit"]??20);
+ $data = $filter->fetchFiltered($selected,$search,$limit);
+ echo json_encode(["rows"=>$data,"search"=>$search]);
+ exit;
+}
+
+/* =======================================================
+ Page HTML
+ ======================================================= */
+$lang = $_GET["lang"]??"en";
+$labels = ["en"=>["Remote IP","Reverse","Creation"],"fr"=>["IP distante","Reverse","Création"]];
+$colKeys = ["remoteip","reverse","creation"];
+$L = $labels[$lang];
+?>
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Super-DB Paginated Highlight</title>
+<style>
+.table-filter-row { display:flex; gap:8px; align-items:center; margin-bottom:6px; flex-wrap: wrap; }
+#langSelect-row { display:block; margin-bottom:12px; }
+table{border-collapse:collapse;width:100%;}
+th,td{padding:6px;border:1px solid #888;}
+.filter-col{cursor:pointer;color:#003;background:#f7f7ff;}
+.filter-col.active{background:#cce;}
+.highlight{background:#ff9;color:#000;}
+</style>
+</head>
+<body>
+
+<!-- Ligne 1 : Langue -->
+<div id="langSelect-row">
+ <select id="langSelect" onchange="window.location='?lang='+this.value;">
+ <option value="en" <?= $lang==="en"?"selected":"" ?>>English</option>
+ <option value="fr" <?= $lang==="fr"?"selected":"" ?>>Français</option>
+ </select>
+</div>
+
+<!-- Ligne 2 : Recherche + Pagination -->
+<div class="table-filter-row">
+ <input type="text" id="searchInput" placeholder="Type to filter…">
+ <select id="limitSelect">
+ <?php foreach([5,10,20,40,80] as $n): ?>
+ <option value="<?= $n ?>" <?= $n===20?"selected":"" ?>><?= $n ?></option>
+ <?php endforeach; ?>
+ </select>
+</div>
+
+<!-- Tableau -->
+<table id="dataTable">
+ <thead>
+ <tr>
+ <?php foreach($L as $i=>$label): ?>
+ <th class="filter-col" data-col="<?= $colKeys[$i] ?>"><?= htmlspecialchars($label) ?></th>
+ <?php endforeach; ?>
+ </tr>
+ </thead>
+ <tbody></tbody>
+</table>
+
+<script>
+let columns=[];
+let currentSearch="";
+
+const searchInput = document.getElementById("searchInput");
+const limitSelect = document.getElementById("limitSelect");
+
+// Header click pour multi-colonnes
+document.querySelectorAll(".filter-col").forEach(th=>{
+ th.addEventListener("click",()=>{
+ const col = th.dataset.col;
+ if(columns.includes(col)){
+ columns = columns.filter(c=>c!==col);
+ th.classList.remove("active");
+ } else {
+ columns.push(col);
+ th.classList.add("active");
+ }
+ refresh();
+ });
+});
+
+// Recherche et changement de limit
+searchInput.addEventListener("input",refresh);
+limitSelect.addEventListener("change",refresh);
+
+// AJAX vers le même fichier
+function refresh(){
+ currentSearch = searchInput.value.trim();
+ const form = new FormData();
+ form.append("search", currentSearch);
+ form.append("limit", limitSelect.value);
+
+ const activeColumns = columns.length ? columns : ["remoteip","reverse","creation"];
+ activeColumns.forEach(c=>form.append("columns[]",c));
+
+ fetch("", {method:"POST", body:form})
+ .then(r=>r.json())
+ .then(json=>renderTable(json.rows,json.search));
+}
+
+// Remplissage tableau avec surbrillance
+function renderTable(rows,search){
+ console.log("AJAX: nombre de lignes reçues =", rows.length);
+ const tbody=document.querySelector("#dataTable tbody");
+ tbody.innerHTML="";
+ const re = search ? new RegExp(search,"gi") : null;
+ for(const row of rows){
+ const tr=document.createElement("tr");
+ const cells = ["remoteip","reverse","creation"].map(c=>{
+ // NULL -> "NULL"
+ let val = row[c] === null ? "NULL" : row[c];
+ if(re) val = val.replace(re,m=>`<span class="highlight">${m}</span>`);
+ return `<td>${val}</td>`;
+ });
+ tr.innerHTML=cells.join("");
+ tbody.appendChild(tr);
+ }
+}
+
+// Load initial
+refresh();
+</script>
+
+</body>
+</html>
+
--- /dev/null
+require_once 'TableFilter.php';
+$pdo = new PDO("pgsql:host=localhost;dbname=mailleur","mailleur","mailleurpass", [
+ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
+]);
+
+$filter = new TableFilter($pdo, "actions", ["remoteip","reverse","creation"]);
+
+// Exemple simple :
+$rows = $filter->fetchFiltered(["remoteip","reverse"], "localhost", 20);
+
+// Ensuite ton PHP construit son HTML/JS à partir de $rows
+
--- /dev/null
+<?php
+/* =======================================================
+ TableFilter.php -->gestbl.php
+ Generic and autonomous class for Mailleur
+ ======================================================= */
+class TableFilter
+{
+ private PDO $pdo;
+ private string $table;
+ private array $columns;
+
+ /**
+ * Constructor
+ * @param PDO $pdo PDO connection
+ * @param string $table Table name
+ * @param array $columns Columns available for filtering
+ */
+ public function __construct(PDO $pdo, string $table, array $columns)
+ {
+ $this->pdo = $pdo;
+ $this->table = $table;
+ $this->columns = $columns;
+ }
+
+ /**
+ * Fetch filtered rows from the table
+ *
+ * @param array $selectedColumns Columns to filter (subset of $this->columns)
+ * @param string $search Search text
+ * @param int $limit Number of rows to return
+ * @return array Associative array of rows
+ */
+ public function fetchFiltered(array $selectedColumns, string $search = '', int $limit = 20): array
+ {
+ // If no columns selected, use all
+ if (empty($selectedColumns)) $selectedColumns = $this->columns;
+
+ // If search is empty, just SELECT with LIMIT
+ if (trim($search) === '') {
+ $sql = "SELECT * FROM {$this->table} ORDER BY 1 LIMIT :limit";
+ $stmt = $this->pdo->prepare($sql);
+ $stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
+ $stmt->execute();
+ return $stmt->fetchAll(PDO::FETCH_ASSOC);
+ }
+
+ // Build WHERE clause for multiple columns
+ $whereParts = [];
+ $params = [];
+ foreach ($selectedColumns as $i => $col) {
+ if (!in_array($col, $this->columns, true)) continue;
+ $key = ":s$i";
+ $whereParts[] = "$col ILIKE $key";
+ $params[$key] = "%$search%";
+ }
+ $where = implode(" OR ", $whereParts);
+
+ // Prepare and execute statement
+ $sql = "SELECT * FROM {$this->table} WHERE $where ORDER BY 1 LIMIT :limit";
+ $stmt = $this->pdo->prepare($sql);
+
+ foreach ($params as $k => $v) {
+ $stmt->bindValue($k, $v, PDO::PARAM_STR);
+ }
+ $stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
+
+ $stmt->execute();
+
+ // Return associative array
+ return $stmt->fetchAll(PDO::FETCH_ASSOC);
+ }
+}
+