répertoire: rename search.php, 6-column layout, HTMX filter, faded entries disabled, URL-shareable

This commit is contained in:
Pontoporeia
2026-04-07 13:57:29 +02:00
parent 088324cb80
commit 572ef75a1e
10 changed files with 522 additions and 197 deletions

View File

@@ -493,6 +493,143 @@ class Database {
return $stmt->fetchAll();
}
/**
* Compute répertoire filter data.
*
* Given a set of active filters (each an array of values, combined as AND
* across filter types, OR within each filter type), returns:
* - matched_ids : int[] thesis IDs matching ALL active filters
* - years : array all years with matched flag
* - ap_programs : array all AP programs with matched flag
* - orientations : array all orientations with matched flag
* - finality_types: array all finality types with matched flag
* - keywords : array all used keywords with matched flag
* - students : array [id, authors] rows for matched theses only
*
* For each column, "matched" means the value appears in at least one thesis
* that satisfies all the OTHER active filters (excluding that column's own
* filter when computing its own relevance).
*
* @param array{years:int[], ap:string[], or:string[], fi:string[], kw:string[]} $filters
*/
public function getRepertoireFilterData(array $filters): array {
$baseJoins = "
FROM theses t
LEFT JOIN orientations o ON t.orientation_id = o.id
LEFT JOIN ap_programs ap ON t.ap_program_id = ap.id
LEFT JOIN finality_types ft ON t.finality_id = ft.id
";
// Build WHERE + bindings excluding one dimension (for that column's own relevance)
$buildWhere = function(string $exclude) use ($filters): array {
$conditions = ['t.is_published = 1'];
$bindings = [];
if ($exclude !== 'years' && !empty($filters['years'])) {
$ph = implode(',', array_fill(0, count($filters['years']), '?'));
$conditions[] = "t.year IN ($ph)";
foreach ($filters['years'] as $v) $bindings[] = (int)$v;
}
if ($exclude !== 'ap' && !empty($filters['ap'])) {
$ph = implode(',', array_fill(0, count($filters['ap']), '?'));
$conditions[] = "ap.name IN ($ph)";
foreach ($filters['ap'] as $v) $bindings[] = (string)$v;
}
if ($exclude !== 'or' && !empty($filters['or'])) {
$ph = implode(',', array_fill(0, count($filters['or']), '?'));
$conditions[] = "o.name IN ($ph)";
foreach ($filters['or'] as $v) $bindings[] = (string)$v;
}
if ($exclude !== 'fi' && !empty($filters['fi'])) {
$ph = implode(',', array_fill(0, count($filters['fi']), '?'));
$conditions[] = "ft.name IN ($ph)";
foreach ($filters['fi'] as $v) $bindings[] = (string)$v;
}
if ($exclude !== 'kw' && !empty($filters['kw'])) {
foreach ($filters['kw'] as $kv) {
$conditions[] = 'EXISTS (SELECT 1 FROM thesis_tags tt2 JOIN tags tg2 ON tg2.id=tt2.tag_id WHERE tt2.thesis_id=t.id AND tg2.name=?)';
$bindings[] = (string)$kv;
}
}
return [implode(' AND ', $conditions), $bindings];
};
$exec = function(string $sql, array $b): array {
$stmt = $this->pdo->prepare($sql);
$stmt->execute($b);
return $stmt->fetchAll();
};
// Full intersection — matched thesis IDs
[$wAll, $bAll] = $buildWhere('__none__');
$matchedIds = array_column($exec("SELECT t.id $baseJoins WHERE $wAll", $bAll), 'id');
// Years
[$w, $b] = $buildWhere('years');
$matchedYears = array_column($exec("SELECT DISTINCT t.year $baseJoins WHERE $w ORDER BY t.year DESC", $b), 'year');
$allYears = array_column($exec("SELECT DISTINCT year FROM theses WHERE is_published=1 ORDER BY year DESC", []), 'year');
$yearsOut = array_map(fn($y) => ['value' => $y, 'matched' => in_array($y, $matchedYears, true)], $allYears);
// AP programs
[$w, $b] = $buildWhere('ap');
$matchedAp = array_column($exec("SELECT DISTINCT ap.name $baseJoins WHERE $w AND ap.name IS NOT NULL ORDER BY ap.name", $b), 'name');
$allAp = array_column($exec("SELECT name FROM ap_programs ORDER BY name", []), 'name');
$apOut = array_map(fn($n) => ['value' => $n, 'matched' => in_array($n, $matchedAp, true)], $allAp);
// Orientations
[$w, $b] = $buildWhere('or');
$matchedOr = array_column($exec("SELECT DISTINCT o.name $baseJoins WHERE $w AND o.name IS NOT NULL ORDER BY o.name", $b), 'name');
$allOr = array_column($exec("SELECT name FROM orientations ORDER BY name", []), 'name');
$orOut = array_map(fn($n) => ['value' => $n, 'matched' => in_array($n, $matchedOr, true)], $allOr);
// Finality types
[$w, $b] = $buildWhere('fi');
$matchedFi = array_column($exec("SELECT DISTINCT ft.name $baseJoins WHERE $w AND ft.name IS NOT NULL ORDER BY ft.name", $b), 'name');
$allFi = array_column($exec("SELECT name FROM finality_types ORDER BY name", []), 'name');
$fiOut = array_map(fn($n) => ['value' => $n, 'matched' => in_array($n, $matchedFi, true)], $allFi);
// Keywords
[$w, $b] = $buildWhere('kw');
$matchedKw = array_column($exec(
"SELECT DISTINCT tg.name $baseJoins
JOIN thesis_tags tt ON tt.thesis_id = t.id
JOIN tags tg ON tg.id = tt.tag_id
WHERE $w ORDER BY tg.name", $b), 'name');
$allKw = array_column($exec(
"SELECT DISTINCT tg.name FROM tags tg
JOIN thesis_tags tt ON tg.id = tt.tag_id
JOIN theses th ON tt.thesis_id = th.id
WHERE th.is_published = 1 ORDER BY tg.name", []), 'name');
$kwOut = array_map(fn($n) => ['value' => $n, 'matched' => in_array($n, $matchedKw, true)], $allKw);
// Students (output only — full intersection)
$studentsOut = [];
if (!empty($matchedIds)) {
$ph = implode(',', array_fill(0, count($matchedIds), '?'));
$studentsOut = $exec(
"SELECT t.id,
GROUP_CONCAT(a.name ORDER BY ta.author_order ASC) AS authors
FROM theses t
JOIN thesis_authors ta ON ta.thesis_id = t.id
JOIN authors a ON a.id = ta.author_id
WHERE t.id IN ($ph)
GROUP BY t.id
ORDER BY MIN(a.name) ASC",
$matchedIds
);
}
return [
'matched_ids' => $matchedIds,
'years' => $yearsOut,
'ap_programs' => $apOut,
'orientations' => $orOut,
'finality_types' => $fiOut,
'keywords' => $kwOut,
'students' => $studentsOut,
];
}
/**
* Get all format types
*/