{% extends 'base.html.twig' %}
{% block title %}Munganyo - Recherche d'actes et publications{% endblock %}
{% block body %}
{# ================================================================
MUNGANYO — Moteur Ibunas.IA
CDC: 3 filtres obligatoires + enrichissement IA (Premium)
Institutions, Chronologique, Type d'acte/annonces
JS dynamique: debounce, AJAX, filter chips, autocomplete
================================================================ #}
<!-- Hero Section Munganyo -->
<section class="relative bg-gradient-to-br from-[#004d26] via-[#006633] to-[#008040] text-white overflow-hidden">
<div class="absolute inset-0 opacity-10 pointer-events-none">
<div class="absolute top-10 left-0 w-80 h-80 bg-white rounded-full blur-3xl"></div>
<div class="absolute bottom-0 right-0 w-[30rem] h-[30rem] bg-[#00994d] rounded-full blur-3xl"></div>
<!-- Pattern géométrique subtil -->
<svg class="absolute inset-0 w-full h-full opacity-20" xmlns="http://www.w3.org/2000/svg">
<pattern id="hex" x="0" y="0" width="40" height="40" patternUnits="userSpaceOnUse">
<path d="M20 2 L38 12 L38 28 L20 38 L2 28 L2 12 Z" fill="none" stroke="white" stroke-width="0.5"/>
</pattern>
<rect width="100%" height="100%" fill="url(#hex)" />
</svg>
</div>
<div class="container mx-auto px-4 py-16 relative z-10">
<div class="max-w-4xl mx-auto text-center">
<!-- Badge moteur -->
<div class="inline-flex items-center bg-white/15 backdrop-blur-sm rounded-full px-5 py-2 mb-5 border border-white/25">
<span class="w-2 h-2 rounded-full bg-emerald-300 animate-pulse mr-2"></span>
<span class="text-sm font-semibold tracking-wide">Moteur Ibunas.IA
{% if isPremium is defined and isPremium %}
<span class="ml-2 bg-amber-400 text-amber-900 text-xs font-bold px-2 py-0.5 rounded-full">PREMIUM</span>
{% endif %}
</span>
</div>
<h1 class="text-5xl md:text-6xl font-black mb-4 tracking-tight">
Munganyo
<span class="block text-2xl md:text-3xl font-light opacity-90 mt-2">Recherche avancée · Journal Officiel des Comores</span>
</h1>
<p class="text-white/80 mb-8 max-w-2xl mx-auto text-base leading-relaxed">
Accédez à l'ensemble des textes juridiques, actes officiels et annonces légales publiés au Journal Officiel de l'Union des Comores.
</p>
<!-- Barre de recherche principale -->
<div class="max-w-2xl mx-auto" id="hero-search-wrapper">
<div class="relative">
<i class="fas fa-search absolute left-5 top-1/2 -translate-y-1/2 text-gray-400 text-lg z-10"></i>
<input
type="text"
id="main-search-input"
name="q"
value="{{ query|default('') }}"
placeholder="Rechercher un décret, un arrêté, une annonce légale..."
autocomplete="off"
class="w-full pl-14 pr-36 py-5 text-gray-800 text-base rounded-full border-none shadow-2xl focus:outline-none focus:ring-4 focus:ring-[#006633]/40 transition-all duration-300"
>
<button id="hero-search-btn"
class="absolute right-2 top-1/2 -translate-y-1/2 bg-gradient-to-r from-[#006633] to-[#008040] text-white px-6 py-2.5 rounded-full font-semibold hover:from-[#004d26] hover:to-[#006633] transition-all duration-300 shadow-md hover:shadow-lg">
<span class="hidden md:inline"><i class="fas fa-arrow-right mr-1"></i> Rechercher</span>
<span class="md:hidden"><i class="fas fa-arrow-right"></i></span>
</button>
<!-- Autocomplete dropdown -->
<div id="autocomplete-dropdown"
class="absolute top-full left-0 right-0 mt-2 bg-white rounded-2xl shadow-2xl border border-gray-100 hidden z-50 overflow-hidden">
</div>
</div>
<!-- Suggestions rapides -->
<div class="flex flex-wrap justify-center gap-2 mt-4">
<span class="text-white/50 text-xs">Tendances :</span>
{% for suggestion in ['Loi de finances', 'Décret nomination', 'Marché public', 'Acte foncier Moroni', 'Récépissé association'] %}
<button type="button" class="quick-suggestion text-white/80 hover:text-white text-xs underline-offset-2 hover:underline transition">{{ suggestion }}</button>
{% endfor %}
</div>
</div>
<!-- Filtres rapides par type -->
<div class="mt-8 flex flex-wrap justify-center gap-2" id="quick-type-filters">
{% set quickTypes = [
{val: '', label: 'Tous', icon: 'fa-layer-group'},
{val: 'loi', label: 'Lois', icon: 'fa-gavel'},
{val: 'decret', label: 'Décrets', icon: 'fa-file-signature'},
{val: 'arrete', label: 'Arrêtés', icon: 'fa-clipboard-list'},
{val: 'marche_public', label: 'Marchés publics', icon: 'fa-briefcase'},
{val: 'acte_foncier', label: 'Foncier', icon: 'fa-map-marker-alt'},
{val: 'enchere', label: 'Enchères', icon: 'fa-hammer'},
] %}
{% for qt in quickTypes %}
<button type="button"
data-type="{{ qt.val }}"
class="quick-type-btn px-4 py-2 rounded-full text-sm font-medium transition-all duration-200
{% if (typeFilter is defined and typeFilter == qt.val) or (qt.val == '' and (typeFilter is not defined or typeFilter == '')) %}
bg-white text-[#006633] shadow-lg
{% else %}
bg-white/20 text-white hover:bg-white/30 border border-white/20
{% endif %}">
<i class="fas {{ qt.icon }} mr-1 text-xs"></i> {{ qt.label }}
</button>
{% endfor %}
</div>
</div>
</div>
<div class="absolute bottom-0 left-0 right-0">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1440 80" class="w-full">
<path fill="#f3f4f6" fill-opacity="1" d="M0,40L60,45C120,50,240,60,360,62C480,64,600,58,720,52C840,46,960,40,1080,42C1200,44,1320,54,1380,58L1440,62L1440,80L0,80Z"></path>
</svg>
</div>
</section>
<!-- ============================================================
FILTRES AVANCÉS (CDC: 3 filtres officiels + 2 supplémentaires)
============================================================ -->
<section class="py-8 bg-gray-50" id="filters-section">
<div class="container mx-auto px-4">
<div class="max-w-6xl mx-auto">
<!-- Header filtres + toggle -->
<div class="flex flex-wrap items-center justify-between gap-3 mb-4">
<div class="flex items-center gap-3">
<h2 class="text-sm font-bold text-gray-700 uppercase tracking-widest">
<i class="fas fa-sliders-h text-[#006633] mr-2"></i> Affiner la recherche
</h2>
<!-- Compteur filtres actifs -->
<span id="active-filters-count"
class="hidden bg-[#006633] text-white text-xs font-bold px-2.5 py-1 rounded-full">
0
</span>
</div>
<div class="flex items-center gap-3">
<button id="toggle-filters-btn"
class="flex items-center gap-2 text-sm text-gray-600 hover:text-[#006633] transition font-medium">
<i class="fas fa-chevron-down transition-transform duration-300" id="toggle-filters-icon"></i>
<span id="toggle-filters-label">Masquer les filtres</span>
</button>
<button id="reset-filters-btn"
class="hidden text-sm text-red-500 hover:text-red-700 transition font-medium">
<i class="fas fa-times-circle mr-1"></i> Réinitialiser
</button>
</div>
</div>
<!-- Chips des filtres actifs -->
<div id="active-filter-chips" class="flex flex-wrap gap-2 mb-4 empty:hidden"></div>
<!-- Panel des filtres -->
<div id="filters-panel" class="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden transition-all duration-300">
<form id="advanced-search-form" class="p-5">
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5 gap-4">
<!-- FILTRE 1 : Institution émettrice (CDC) -->
<div class="xl:col-span-2">
<label class="block text-xs font-bold text-gray-500 uppercase tracking-wider mb-2">
<i class="fas fa-landmark text-[#006633] mr-1"></i> Institution émettrice
</label>
<select name="institution" id="filter-institution"
class="search-filter w-full border border-gray-200 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-[#006633]/40 bg-white">
<option value="">Toutes les institutions</option>
<optgroup label="🇰🇲 Union des Comores">
<option value="presidence" {% if filters.institution is defined and filters.institution == 'presidence' %}selected{% endif %}>Présidence de l'Union</option>
<option value="assemblee_nationale" {% if filters.institution is defined and filters.institution == 'assemblee_nationale' %}selected{% endif %}>Assemblée Nationale</option>
<option value="sgg" {% if filters.institution is defined and filters.institution == 'sgg' %}selected{% endif %}>Secrétariat Général du Gouvernement</option>
<option value="vice_presidence" {% if filters.institution is defined and filters.institution == 'vice_presidence' %}selected{% endif %}>Vice-Présidence</option>
<option value="finances" {% if filters.institution is defined and filters.institution == 'finances' %}selected{% endif %}>Ministère des Finances</option>
<option value="justice" {% if filters.institution is defined and filters.institution == 'justice' %}selected{% endif %}>Ministère de la Justice</option>
<option value="interieur" {% if filters.institution is defined and filters.institution == 'interieur' %}selected{% endif %}>Ministère de l'Intérieur</option>
<option value="sante" {% if filters.institution is defined and filters.institution == 'sante' %}selected{% endif %}>Ministère de la Santé</option>
<option value="education" {% if filters.institution is defined and filters.institution == 'education' %}selected{% endif %}>Ministère de l'Éducation</option>
<option value="transport" {% if filters.institution is defined and filters.institution == 'transport' %}selected{% endif %}>Ministère du Transport</option>
<option value="agriculture" {% if filters.institution is defined and filters.institution == 'agriculture' %}selected{% endif %}>Ministère de l'Agriculture</option>
<option value="travaux_publics" {% if filters.institution is defined and filters.institution == 'travaux_publics' %}selected{% endif %}>Ministère des Travaux Publics</option>
<option value="autres_ministeres" {% if filters.institution is defined and filters.institution == 'autres_ministeres' %}selected{% endif %}>Autres ministères</option>
</optgroup>
<optgroup label="🏛 Gouvernorats">
<option value="gouvernorat_ngazidja" {% if filters.institution is defined and filters.institution == 'gouvernorat_ngazidja' %}selected{% endif %}>Gouvernorat de Ngazidja (Grande Comore)</option>
<option value="gouvernorat_anjouan" {% if filters.institution is defined and filters.institution == 'gouvernorat_anjouan' %}selected{% endif %}>Gouvernorat d'Anjouan (Ndzuani)</option>
<option value="gouvernorat_moheli" {% if filters.institution is defined and filters.institution == 'gouvernorat_moheli' %}selected{% endif %}>Gouvernorat de Mohéli (Mwali)</option>
</optgroup>
<optgroup label="⚖ Juridictions">
<option value="justice_cour_supreme" {% if filters.institution is defined and filters.institution == 'justice_cour_supreme' %}selected{% endif %}>Cour Suprême</option>
<option value="justice_cour_appel" {% if filters.institution is defined and filters.institution == 'justice_cour_appel' %}selected{% endif %}>Cour d'Appel</option>
</optgroup>
</select>
</div>
<!-- FILTRE 2 : Chronologique (CDC) -->
<div>
<label class="block text-xs font-bold text-gray-500 uppercase tracking-wider mb-2">
<i class="fas fa-calendar-alt text-[#006633] mr-1"></i> Période
</label>
<select name="period" id="filter-period"
class="search-filter w-full border border-gray-200 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-[#006633]/40 bg-white">
<option value="">Toutes périodes</option>
{% for yr in 2026..2018 %}
<option value="{{ yr }}" {% if filters.period is defined and filters.period == (yr~'') %}selected{% endif %}>{{ yr }}</option>
{% endfor %}
<option value="custom" {% if filters.period is defined and filters.period == 'custom' %}selected{% endif %}>Période personnalisée</option>
</select>
<!-- Dates personnalisées -->
<div id="custom-dates-panel" class="mt-2 space-y-1.5 {% if filters.period is not defined or filters.period != 'custom' %}hidden{% endif %}">
<input type="date" name="date_debut" id="filter-date-debut"
value="{{ filters.date_debut|default('') }}"
class="search-filter w-full border border-gray-200 rounded-xl px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[#006633]/40">
<input type="date" name="date_fin" id="filter-date-fin"
value="{{ filters.date_fin|default('') }}"
class="search-filter w-full border border-gray-200 rounded-xl px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[#006633]/40">
</div>
</div>
<!-- FILTRE 3 : Type d'acte / annonces (CDC) -->
<div>
<label class="block text-xs font-bold text-gray-500 uppercase tracking-wider mb-2">
<i class="fas fa-file-alt text-[#006633] mr-1"></i> Type d'acte
</label>
<select name="type" id="filter-type"
class="search-filter w-full border border-gray-200 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-[#006633]/40 bg-white">
<option value="">Tous les types</option>
<optgroup label="📋 Actes officiels">
<option value="loi" {% if filters.type is defined and filters.type == 'loi' %}selected{% endif %}>Lois</option>
<option value="decret" {% if filters.type is defined and filters.type == 'decret' %}selected{% endif %}>Décrets</option>
<option value="arrete" {% if filters.type is defined and filters.type == 'arrete' %}selected{% endif %}>Arrêtés</option>
<option value="circulaire" {% if filters.type is defined and filters.type == 'circulaire' %}selected{% endif %}>Circulaires</option>
<option value="avis" {% if filters.type is defined and filters.type == 'avis' %}selected{% endif %}>Avis officiels</option>
</optgroup>
<optgroup label="📢 Annonces légales">
<option value="marche_public" {% if filters.type is defined and filters.type == 'marche_public' %}selected{% endif %}>Marchés publics & DSP</option>
<option value="acte_foncier" {% if filters.type is defined and filters.type == 'acte_foncier' %}selected{% endif %}>Réquisitions & actes fonciers</option>
<option value="societe_rccm" {% if filters.type is defined and filters.type == 'societe_rccm' %}selected{% endif %}>Actes RCCM/OHADA (sociétés)</option>
<option value="enchere" {% if filters.type is defined and filters.type == 'enchere' %}selected{% endif %}>Ventes aux enchères publiques</option>
<option value="association" {% if filters.type is defined and filters.type == 'association' %}selected{% endif %}>Récépissés associations</option>
</optgroup>
</select>
</div>
<!-- FILTRE 4 : Statut juridique (CDC: En vigueur / Abrogé…) -->
<div>
<label class="block text-xs font-bold text-gray-500 uppercase tracking-wider mb-2">
<i class="fas fa-balance-scale text-[#006633] mr-1"></i> Statut juridique
</label>
<select name="statut" id="filter-statut"
class="search-filter w-full border border-gray-200 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-[#006633]/40 bg-white">
<option value="">Tous statuts</option>
<option value="en_vigueur" {% if filters.statut is defined and filters.statut == 'en_vigueur' %}selected{% endif %}>✅ En vigueur</option>
<option value="modifie" {% if filters.statut is defined and filters.statut == 'modifie' %}selected{% endif %}>✏️ Modifié</option>
<option value="abroge" {% if filters.statut is defined and filters.statut == 'abroge' %}selected{% endif %}>❌ Abrogé</option>
<option value="suspendu" {% if filters.statut is defined and filters.statut == 'suspendu' %}selected{% endif %}>⏸️ Suspendu</option>
<option value="remplace" {% if filters.statut is defined and filters.statut == 'remplace' %}selected{% endif %}>🔄 Remplacé</option>
<option value="archive" {% if filters.statut is defined and filters.statut == 'archive' %}selected{% endif %}>📦 Archivé</option>
</select>
</div>
</div>
<!-- Ligne 2 : Territoire + tri + submit -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 mt-4 pt-4 border-t border-gray-100">
<!-- Territoire applicable (CDC) -->
<div>
<label class="block text-xs font-bold text-gray-500 uppercase tracking-wider mb-2">
<i class="fas fa-map-marker-alt text-[#006633] mr-1"></i> Territoire applicable
</label>
<select name="territoire" id="filter-territoire"
class="search-filter w-full border border-gray-200 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-[#006633]/40 bg-white">
<option value="">Toute l'Union</option>
<option value="union" {% if filters.territoire is defined and filters.territoire == 'union' %}selected{% endif %}>Union des Comores</option>
<option value="ngazidja" {% if filters.territoire is defined and filters.territoire == 'ngazidja' %}selected{% endif %}>Grande Comore (Ngazidja)</option>
<option value="anjouan" {% if filters.territoire is defined and filters.territoire == 'anjouan' %}selected{% endif %}>Anjouan (Ndzuani)</option>
<option value="moheli" {% if filters.territoire is defined and filters.territoire == 'moheli' %}selected{% endif %}>Mohéli (Mwali)</option>
<option value="moroni" {% if filters.territoire is defined and filters.territoire == 'moroni' %}selected{% endif %}>Moroni</option>
<option value="mutsamudu" {% if filters.territoire is defined and filters.territoire == 'mutsamudu' %}selected{% endif %}>Mutsamudu</option>
<option value="fomboni" {% if filters.territoire is defined and filters.territoire == 'fomboni' %}selected{% endif %}>Fomboni</option>
</select>
</div>
<!-- Tri -->
<div>
<label class="block text-xs font-bold text-gray-500 uppercase tracking-wider mb-2">
<i class="fas fa-sort text-[#006633] mr-1"></i> Trier par
</label>
<select name="sort" id="filter-sort"
class="search-filter w-full border border-gray-200 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-[#006633]/40 bg-white">
<option value="date_desc" {% if filters.sort is not defined or filters.sort == 'date_desc' %}selected{% endif %}>Date (récent → ancien)</option>
<option value="date_asc" {% if filters.sort is defined and filters.sort == 'date_asc' %}selected{% endif %}>Date (ancien → récent)</option>
<option value="pertinence" {% if filters.sort is defined and filters.sort == 'pertinence' %}selected{% endif %}>Pertinence</option>
<option value="titre_asc" {% if filters.sort is defined and filters.sort == 'titre_asc' %}selected{% endif %}>Titre A → Z</option>
</select>
</div>
<!-- Actions -->
<div class="flex items-end gap-3">
<button type="submit" id="apply-filters-btn"
class="flex-1 bg-gradient-to-r from-[#006633] to-[#008040] text-white px-5 py-2.5 rounded-xl hover:from-[#004d26] hover:to-[#006633] transition shadow-md font-semibold text-sm">
<i class="fas fa-search mr-2"></i> Appliquer
</button>
<a href="{{ path('app_search') }}"
class="px-4 py-2.5 border border-gray-200 rounded-xl text-gray-500 hover:text-gray-700 hover:border-gray-300 transition text-sm font-medium">
<i class="fas fa-undo-alt"></i>
</a>
</div>
</div>
</form>
</div>
</div>
</div>
</section>
<!-- ============================================================
ZONE DE RÉSULTATS
============================================================ -->
<section class="py-10 bg-white min-h-[40vh]" id="results-section">
<div class="container mx-auto px-4">
<div class="max-w-6xl mx-auto">
<!-- Barre de statut résultats -->
<div id="results-status-bar" class="flex flex-wrap items-center justify-between gap-3 mb-6 {% if query is not defined or query is empty %}hidden{% endif %}">
<div class="flex items-center gap-3">
<!-- Spinner loading (caché par défaut) -->
<div id="search-spinner" class="hidden">
<svg class="animate-spin w-5 h-5 text-[#006633]" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
</svg>
</div>
<h2 class="text-base font-bold text-gray-800" id="results-count-label">
{% if query is defined and query is not empty %}
<span id="count-number">{{ results|length }}</span> résultat(s) pour
<em class="text-[#006633] not-italic font-black">"{{ query }}"</em>
{% endif %}
</h2>
</div>
<!-- Badge IA -->
<div>
{% if isPremium is defined and isPremium %}
<div class="flex items-center gap-2 bg-gradient-to-r from-amber-50 to-yellow-50 border border-amber-200 text-amber-800 px-4 py-1.5 rounded-full text-sm font-semibold">
<i class="fas fa-microchip text-amber-500"></i>
Recherche enrichie Ibunas.IA activée
</div>
{% else %}
<a href="{{ path('app_abonn') }}"
class="flex items-center gap-2 bg-gray-50 border border-gray-200 text-gray-600 hover:border-[#006633] hover:text-[#006633] px-4 py-1.5 rounded-full text-sm font-medium transition">
<i class="fas fa-lock text-xs"></i>
Activer Ibunas.IA Premium
</a>
{% endif %}
</div>
</div>
<!-- Liste des résultats (rendu SSR initial + remplacement AJAX) -->
<div id="results-container">
{% if query is defined and query is not empty %}
{% if results is defined and results|length > 0 %}
{% include '_partials/search_results_list.html.twig' with {results: results, isPremium: isPremium} %}
{% else %}
{% include '_partials/search_no_results.html.twig' with {query: query} %}
{% endif %}
{% else %}
<!-- État d'accueil -->
<div class="text-center py-16 bg-gradient-to-br from-gray-50 to-emerald-50/30 rounded-2xl border border-gray-100" id="welcome-state">
<div class="w-20 h-20 bg-gradient-to-br from-[#006633] to-[#008040] rounded-2xl flex items-center justify-center mx-auto mb-5 shadow-lg">
<i class="fas fa-search text-2xl text-white"></i>
</div>
<h2 class="text-2xl font-bold text-gray-800 mb-2">Que recherchez-vous ?</h2>
<p class="text-gray-500 max-w-md mx-auto mb-6 text-sm">
Textes législatifs, décrets présidentiels, arrêtés ministériels,
marchés publics, actes fonciers… tout le Journal Officiel des Comores en un clic.
</p>
<div class="flex flex-wrap justify-center gap-3 text-xs text-gray-500">
<span class="px-3 py-1.5 bg-white rounded-full shadow-sm border border-gray-100">
<i class="fas fa-gavel text-purple-500 mr-1"></i> "Loi n°2026-…"
</span>
<span class="px-3 py-1.5 bg-white rounded-full shadow-sm border border-gray-100">
<i class="fas fa-file-signature text-blue-500 mr-1"></i> "Décret nomination ministre"
</span>
<span class="px-3 py-1.5 bg-white rounded-full shadow-sm border border-gray-100">
<i class="fas fa-map-marker-alt text-teal-500 mr-1"></i> "Actes fonciers Moroni"
</span>
<span class="px-3 py-1.5 bg-white rounded-full shadow-sm border border-gray-100">
<i class="fas fa-briefcase text-emerald-500 mr-1"></i> "Appel d'offres travaux routiers"
</span>
</div>
</div>
{% endif %}
</div>
<!-- Pagination (rendu SSR + remplacé par AJAX) -->
<div id="pagination-container">
{% if totalPages is defined and totalPages > 1 %}
<nav class="flex justify-center mt-8" aria-label="Pagination">
<ul class="flex items-center gap-1">
<li>
<a href="{{ currentPage > 1 ? path('app_search', {page: currentPage - 1, q: query}) : '#' }}"
class="flex items-center justify-center w-9 h-9 rounded-lg border border-gray-200 text-gray-500 hover:border-[#006633] hover:text-[#006633] transition text-sm {% if currentPage <= 1 %}opacity-40 pointer-events-none{% endif %}">
<i class="fas fa-chevron-left text-xs"></i>
</a>
</li>
{% for page in 1..totalPages %}
{% if page == currentPage %}
<li><span class="flex items-center justify-center w-9 h-9 rounded-lg bg-[#006633] text-white font-bold text-sm">{{ page }}</span></li>
{% elseif page <= 3 or page >= totalPages - 2 or (page >= currentPage - 2 and page <= currentPage + 2) %}
<li>
<a href="{{ path('app_search', {page: page, q: query}) }}"
class="flex items-center justify-center w-9 h-9 rounded-lg border border-gray-200 text-gray-600 hover:border-[#006633] hover:text-[#006633] transition text-sm">
{{ page }}
</a>
</li>
{% elseif page == 4 and currentPage > 6 %}
<li><span class="px-2 text-gray-400">…</span></li>
{% elseif page == totalPages - 3 and currentPage < totalPages - 5 %}
<li><span class="px-2 text-gray-400">…</span></li>
{% endif %}
{% endfor %}
<li>
<a href="{{ currentPage < totalPages ? path('app_search', {page: currentPage + 1, q: query}) : '#' }}"
class="flex items-center justify-center w-9 h-9 rounded-lg border border-gray-200 text-gray-500 hover:border-[#006633] hover:text-[#006633] transition text-sm {% if currentPage >= totalPages %}opacity-40 pointer-events-none{% endif %}">
<i class="fas fa-chevron-right text-xs"></i>
</a>
</li>
</ul>
</nav>
{% endif %}
</div>
</div>
</div>
</section>
<!-- ============================================================
CONSEILS + STATS (bas de page)
============================================================ -->
<section class="py-10 bg-gray-50 border-t border-gray-100">
<div class="container mx-auto px-4">
<div class="max-w-6xl mx-auto">
<div class="grid grid-cols-1 md:grid-cols-3 gap-5">
<div class="bg-white rounded-xl p-5 shadow-sm border border-gray-100 flex gap-4 items-start">
<div class="w-10 h-10 bg-emerald-100 rounded-xl flex items-center justify-center flex-shrink-0">
<i class="fas fa-lightbulb text-[#006633]"></i>
</div>
<div>
<h3 class="font-semibold text-gray-800 text-sm mb-1">Recherche par mots-clés</h3>
<p class="text-xs text-gray-500">Termes précis : "impôt", "nomination", "foncier", "appel d'offres bâtiment"</p>
</div>
</div>
<div class="bg-white rounded-xl p-5 shadow-sm border border-gray-100 flex gap-4 items-start">
<div class="w-10 h-10 bg-emerald-100 rounded-xl flex items-center justify-center flex-shrink-0">
<i class="fas fa-hashtag text-[#006633]"></i>
</div>
<div>
<h3 class="font-semibold text-gray-800 text-sm mb-1">Référence exacte</h3>
<p class="text-xs text-gray-500">Numéro d'acte : "LOI-2026-015", "Décret 2026-045/PR"</p>
</div>
</div>
<div class="bg-white rounded-xl p-5 shadow-sm border border-gray-100 flex gap-4 items-start">
<div class="w-10 h-10 bg-amber-100 rounded-xl flex items-center justify-center flex-shrink-0">
<i class="fas fa-microchip text-amber-600"></i>
</div>
<div>
<h3 class="font-semibold text-gray-800 text-sm mb-1">Recherche Ibunas.IA <span class="text-amber-600 text-xs font-bold">PREMIUM</span></h3>
<p class="text-xs text-gray-500">Recherche dans le contenu des actes via OCR et intelligence artificielle</p>
</div>
</div>
</div>
</div>
</div>
</section>
{# ================================================================
RÉSULTATS PARTIELS (macros Twig inline pour éviter includes manquants)
En production, extraire dans _partials/
================================================================ #}
{% macro renderResultCard(result, isPremium) %}
{% set typeColors = {
'loi': 'bg-purple-100 text-purple-700 border-purple-200',
'decret': 'bg-blue-100 text-blue-700 border-blue-200',
'arrete': 'bg-green-100 text-green-700 border-green-200',
'circulaire': 'bg-cyan-100 text-cyan-700 border-cyan-200',
'avis': 'bg-sky-100 text-sky-700 border-sky-200',
'marche_public':'bg-emerald-100 text-emerald-700 border-emerald-200',
'acte_foncier': 'bg-teal-100 text-teal-700 border-teal-200',
'societe_rccm': 'bg-indigo-100 text-indigo-700 border-indigo-200',
'association': 'bg-amber-100 text-amber-700 border-amber-200',
'enchere': 'bg-orange-100 text-orange-700 border-orange-200'
} %}
{% set typeIcons = {
'loi': 'fa-gavel', 'decret': 'fa-file-signature',
'arrete': 'fa-clipboard-list', 'circulaire': 'fa-envelope',
'avis': 'fa-bell', 'marche_public': 'fa-briefcase',
'acte_foncier': 'fa-map-marker-alt', 'societe_rccm': 'fa-building',
'association': 'fa-handshake', 'enchere': 'fa-hammer'
} %}
<article class="bg-white border border-gray-100 rounded-2xl shadow-sm hover:shadow-md transition-all duration-200 p-5 group">
<div class="flex flex-wrap justify-between items-start gap-2 mb-3">
<div class="flex flex-wrap items-center gap-2">
<span class="inline-flex items-center gap-1.5 px-3 py-1 text-xs font-bold rounded-full border {{ typeColors[result.type]|default('bg-gray-100 text-gray-700 border-gray-200') }}">
<i class="fas {{ typeIcons[result.type]|default('fa-file-alt') }} text-[10px]"></i>
{{ result.typeLabel|default(result.type|upper) }}
</span>
{% if result.numero is defined and result.numero %}
<span class="text-xs text-gray-400 font-mono bg-gray-50 px-2 py-0.5 rounded">{{ result.numero }}</span>
{% endif %}
{% if result.statutJuridique is defined %}
<span class="text-xs px-2 py-0.5 rounded
{% if result.statutJuridique == 'en_vigueur' %}bg-green-50 text-green-600
{% elseif result.statutJuridique == 'abroge' %}bg-red-50 text-red-600
{% else %}bg-gray-50 text-gray-500{% endif %}">
{{ result.statutJuridique|replace({'_': ' '})|title }}
</span>
{% endif %}
</div>
<time class="text-xs text-gray-400 flex items-center gap-1 flex-shrink-0">
<i class="far fa-calendar-alt"></i>
{{ result.datePublication|date('d/m/Y') }}
</time>
</div>
<h3 class="text-base font-bold text-gray-800 mb-2 group-hover:text-[#006633] transition leading-snug">
<a href="{{ path('app_acte_show', {id: result.id}) }}">{{ result.titre }}</a>
</h3>
{% if result.resume is defined and result.resume %}
<p class="text-gray-500 text-sm mb-3 line-clamp-2">{{ result.resume }}</p>
{% endif %}
{% if isPremium and result.extrait is defined and result.extrait %}
<div class="mb-3 px-3 py-2 bg-amber-50 border-l-3 border-amber-400 rounded-r-lg text-xs text-gray-700">
<i class="fas fa-quote-left text-amber-400 mr-1"></i>
<span>{{ result.extrait }}</span>
</div>
{% endif %}
<div class="flex flex-wrap items-center justify-between gap-2 pt-3 border-t border-gray-50">
<div class="flex items-center gap-3 text-xs text-gray-500">
{% if result.institution is defined %}
<span><i class="fas fa-landmark text-[#006633] mr-1"></i>{{ result.institution }}</span>
{% endif %}
{% if result.territoire is defined %}
<span><i class="fas fa-map-marker-alt text-gray-400 mr-1"></i>{{ result.territoire }}</span>
{% endif %}
{% if isPremium and result.relevance is defined %}
<span class="text-[#006633] font-semibold">
<i class="fas fa-chart-line mr-1"></i>{{ (result.relevance * 100)|round }}% pertinent
</span>
{% endif %}
</div>
<div class="flex items-center gap-3">
<a href="{{ path('app_acte_show', {id: result.id}) }}"
class="text-xs text-[#006633] hover:text-[#008040] font-semibold flex items-center gap-1 transition">
<i class="fas fa-eye"></i> Consulter
</a>
{% if result.pdf_url is defined and result.pdf_url %}
<a href="{{ result.pdf_url }}" target="_blank" rel="noopener"
class="text-xs text-gray-400 hover:text-gray-600 flex items-center gap-1 transition">
<i class="fas fa-file-pdf"></i> PDF
</a>
{% endif %}
</div>
</div>
</article>
{% endmacro %}
<!-- ============================================================
JAVASCRIPT DYNAMIQUE
============================================================ -->
<script>
(function () {
'use strict';
// ─── Config ───────────────────────────────────────────────────────────────
const SEARCH_URL = '{{ path("app_search") }}';
const SEARCH_API_URL = '{{ path("app_search_ajax") }}'; // endpoint JSON
const DEBOUNCE_MS = 400;
const MIN_CHARS = 2;
// ─── Éléments DOM ─────────────────────────────────────────────────────────
const mainInput = document.getElementById('main-search-input');
const heroBtnSearch = document.getElementById('hero-search-btn');
const autocompleteBox = document.getElementById('autocomplete-dropdown');
const resultsContainer = document.getElementById('results-container');
const paginationCont = document.getElementById('pagination-container');
const statusBar = document.getElementById('results-status-bar');
const countNumber = document.getElementById('count-number');
const countLabel = document.getElementById('results-count-label');
const spinner = document.getElementById('search-spinner');
const filterForm = document.getElementById('advanced-search-form');
const filterSelects = filterForm.querySelectorAll('.search-filter');
const resetBtn = document.getElementById('reset-filters-btn');
const applyBtn = document.getElementById('apply-filters-btn');
const activeCountBadge = document.getElementById('active-filters-count');
const filterChips = document.getElementById('active-filter-chips');
const toggleFiltersBtn = document.getElementById('toggle-filters-btn');
const toggleFiltersIcon= document.getElementById('toggle-filters-icon');
const toggleFiltersLbl = document.getElementById('toggle-filters-label');
const filtersPanel = document.getElementById('filters-panel');
const periodSelect = document.getElementById('filter-period');
const customDatesPanel = document.getElementById('custom-dates-panel');
const quickTypeBtns = document.querySelectorAll('.quick-type-btn');
const quickSuggestions = document.querySelectorAll('.quick-suggestion');
// ─── État ─────────────────────────────────────────────────────────────────
let debounceTimer = null;
let currentPage = 1;
let filtersVisible = true;
let lastQuery = mainInput ? mainInput.value.trim() : '';
let currentParams = {};
// ─── Noms lisibles des filtres pour les chips ─────────────────────────────
const filterLabels = {
institution: {
presidence: 'Présidence de l\'Union',
assemblee_nationale: 'Assemblée Nationale',
sgg: 'SGG',
finances: 'Min. Finances',
justice: 'Min. Justice',
interieur: 'Min. Intérieur',
sante: 'Min. Santé',
education: 'Min. Éducation',
gouvernorat_ngazidja: 'Gouvernorat Ngazidja',
gouvernorat_anjouan: 'Gouvernorat Anjouan',
gouvernorat_moheli: 'Gouvernorat Mohéli',
vice_presidence: 'Vice-Présidence',
transport: 'Min. Transport',
agriculture: 'Min. Agriculture',
travaux_publics: 'Min. Travaux Publics',
autres_ministeres: 'Autres ministères',
justice_cour_supreme: 'Cour Suprême',
justice_cour_appel: 'Cour d\'Appel',
},
type: {
loi: 'Lois', decret: 'Décrets', arrete: 'Arrêtés',
circulaire: 'Circulaires', avis: 'Avis officiels',
marche_public: 'Marchés publics & DSP',
acte_foncier: 'Réquisitions & actes fonciers',
societe_rccm: 'Actes RCCM/OHADA',
enchere: 'Ventes aux enchères',
association: 'Récépissés associations',
},
statut: {
en_vigueur: 'En vigueur', modifie: 'Modifié',
abroge: 'Abrogé', suspendu: 'Suspendu',
remplace: 'Remplacé', archive: 'Archivé',
},
territoire: {
union: 'Union des Comores', ngazidja: 'Grande Comore',
anjouan: 'Anjouan', moheli: 'Mohéli',
moroni: 'Moroni', mutsamudu: 'Mutsamudu', fomboni: 'Fomboni',
},
sort: {
date_desc: 'Récent → Ancien', date_asc: 'Ancien → Récent',
pertinence: 'Pertinence', titre_asc: 'Titre A→Z',
},
};
// ─── Utilitaires ──────────────────────────────────────────────────────────
function debounce(fn, delay) {
return function (...args) {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => fn.apply(this, args), delay);
};
}
function getFormParams() {
const params = {};
if (mainInput && mainInput.value.trim()) params.q = mainInput.value.trim();
filterSelects.forEach(el => {
if (el.value) params[el.name] = el.value;
});
return params;
}
function buildQueryString(params) {
return Object.entries(params)
.filter(([, v]) => v)
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
.join('&');
}
// ─── Chips filtres actifs ──────────────────────────────────────────────────
function updateFilterChips() {
if (!filterChips) return;
filterChips.innerHTML = '';
let count = 0;
filterSelects.forEach(el => {
if (!el.value || el.name === 'sort') return;
count++;
const label = (filterLabels[el.name] || {})[el.value] || el.value;
const chip = document.createElement('span');
chip.className = 'inline-flex items-center gap-1.5 bg-[#006633]/10 text-[#006633] border border-[#006633]/20 text-xs font-semibold px-3 py-1 rounded-full';
chip.innerHTML = `${label} <button type="button" data-filter="${el.name}" class="remove-chip ml-0.5 hover:text-red-500 transition"><i class="fas fa-times text-[10px]"></i></button>`;
filterChips.appendChild(chip);
});
// Badge compteur
if (count > 0) {
activeCountBadge.textContent = count;
activeCountBadge.classList.remove('hidden');
resetBtn.classList.remove('hidden');
} else {
activeCountBadge.classList.add('hidden');
resetBtn.classList.add('hidden');
}
// Clics sur remove-chip
filterChips.querySelectorAll('.remove-chip').forEach(btn => {
btn.addEventListener('click', () => {
const filterName = btn.dataset.filter;
const el = filterForm.querySelector(`[name="${filterName}"]`);
if (el) { el.value = ''; }
updateFilterChips();
triggerSearch(1);
});
});
}
// ─── Toggle panel filtres ─────────────────────────────────────────────────
if (toggleFiltersBtn) {
toggleFiltersBtn.addEventListener('click', () => {
filtersVisible = !filtersVisible;
filtersPanel.style.maxHeight = filtersVisible ? filtersPanel.scrollHeight + 'px' : '0';
filtersPanel.style.overflow = filtersVisible ? 'visible' : 'hidden';
toggleFiltersIcon.style.transform = filtersVisible ? 'rotate(0deg)' : 'rotate(-90deg)';
toggleFiltersLbl.textContent = filtersVisible ? 'Masquer les filtres' : 'Afficher les filtres';
});
}
// ─── Période personnalisée ────────────────────────────────────────────────
if (periodSelect) {
periodSelect.addEventListener('change', function () {
customDatesPanel.classList.toggle('hidden', this.value !== 'custom');
});
}
// ─── Boutons filtres rapides (Hero) ───────────────────────────────────────
quickTypeBtns.forEach(btn => {
btn.addEventListener('click', () => {
// Update visuel
quickTypeBtns.forEach(b => {
b.className = b.className.replace('bg-white text-[#006633] shadow-lg', 'bg-white/20 text-white hover:bg-white/30 border border-white/20');
});
btn.className = btn.className.replace('bg-white/20 text-white hover:bg-white/30 border border-white/20', 'bg-white text-[#006633] shadow-lg');
// Synchro avec le select type
const typeFilter = document.getElementById('filter-type');
if (typeFilter) typeFilter.value = btn.dataset.type;
updateFilterChips();
triggerSearch(1);
});
});
// ─── Suggestions rapides ──────────────────────────────────────────────────
quickSuggestions.forEach(btn => {
btn.addEventListener('click', () => {
if (mainInput) mainInput.value = btn.textContent.trim();
triggerSearch(1);
});
});
// ─── Autocomplete ─────────────────────────────────────────────────────────
const debouncedAutocomplete = debounce(fetchAutocomplete, 300);
if (mainInput) {
mainInput.addEventListener('input', function () {
const q = this.value.trim();
if (q.length >= MIN_CHARS) {
debouncedAutocomplete(q);
} else {
hideAutocomplete();
}
// Live search
debouncedSearch();
});
mainInput.addEventListener('keydown', function (e) {
if (e.key === 'Enter') {
e.preventDefault();
hideAutocomplete();
triggerSearch(1);
}
if (e.key === 'Escape') hideAutocomplete();
});
}
if (heroBtnSearch) {
heroBtnSearch.addEventListener('click', () => {
hideAutocomplete();
triggerSearch(1);
});
}
function hideAutocomplete() {
if (autocompleteBox) autocompleteBox.classList.add('hidden');
}
async function fetchAutocomplete(q) {
try {
const res = await fetch(`${SEARCH_API_URL}?q=${encodeURIComponent(q)}&autocomplete=1&limit=6`);
if (!res.ok) return;
const data = await res.json();
renderAutocomplete(data.suggestions || []);
} catch (e) {
// silencieux
}
}
function renderAutocomplete(suggestions) {
if (!autocompleteBox || suggestions.length === 0) {
hideAutocomplete();
return;
}
autocompleteBox.innerHTML = suggestions.map(s => `
<button type="button" class="autocomplete-item w-full text-left px-4 py-3 hover:bg-emerald-50 flex items-center gap-3 border-b border-gray-50 last:border-none transition-colors">
<i class="fas fa-search text-gray-300 text-xs flex-shrink-0"></i>
<span class="text-sm text-gray-700">${escapeHtml(s.titre || s)}</span>
${s.type ? `<span class="ml-auto text-xs text-gray-400 font-mono">${escapeHtml(s.type)}</span>` : ''}
</button>
`).join('');
autocompleteBox.classList.remove('hidden');
autocompleteBox.querySelectorAll('.autocomplete-item').forEach((item, idx) => {
item.addEventListener('click', () => {
const s = suggestions[idx];
if (mainInput) mainInput.value = s.titre || s;
hideAutocomplete();
triggerSearch(1);
});
});
}
document.addEventListener('click', e => {
if (!autocompleteBox?.contains(e.target) && e.target !== mainInput) hideAutocomplete();
});
// ─── Changement de filtre → rechargement live ─────────────────────────────
filterSelects.forEach(el => {
el.addEventListener('change', () => {
updateFilterChips();
if (el.name !== 'date_debut' && el.name !== 'date_fin') triggerSearch(1);
});
});
// Dates personnalisées: déclencher à la perte de focus
['filter-date-debut', 'filter-date-fin'].forEach(id => {
const el = document.getElementById(id);
if (el) el.addEventListener('change', () => triggerSearch(1));
});
// ─── Formulaire submit ────────────────────────────────────────────────────
if (filterForm) {
filterForm.addEventListener('submit', e => {
e.preventDefault();
triggerSearch(1);
});
}
// ─── Recherche principale (AJAX) ──────────────────────────────────────────
const debouncedSearch = debounce(() => triggerSearch(1), DEBOUNCE_MS);
async function triggerSearch(page = 1) {
currentPage = page;
const params = getFormParams();
params.page = page;
currentParams = { ...params };
// Mettre à jour l'URL sans rechargement
const qs = buildQueryString(params);
window.history.replaceState({}, '', `${SEARCH_URL}${qs ? '?' + qs : ''}`);
if (!params.q && Object.keys(params).filter(k => k !== 'sort' && k !== 'page').length === 0) {
// Aucun critère: afficher état accueil
showWelcomeState();
return;
}
showLoading();
try {
const res = await fetch(`${SEARCH_API_URL}?${qs}`, {
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' }
});
// Si la route API n'existe pas encore (404/405/500), fallback SSR silencieux
if (res.status === 404 || res.status === 405) {
window.location.href = `${SEARCH_URL}?${qs}`;
return;
}
if (!res.ok) throw new Error('HTTP ' + res.status);
const data = await res.json();
renderResults(data, params.q || '');
} catch (err) {
// Erreur réseau : fallback vers SSR plutôt qu'afficher un message d'erreur
console.warn('[Munganyo] API indisponible, fallback SSR:', err.message);
window.location.href = `${SEARCH_URL}?${qs}`;
}
}
function showLoading() {
if (spinner) spinner.classList.remove('hidden');
if (statusBar) statusBar.classList.remove('hidden');
if (resultsContainer) {
resultsContainer.style.opacity = '0.4';
resultsContainer.style.pointerEvents = 'none';
}
}
function hideLoading() {
if (spinner) spinner.classList.add('hidden');
if (resultsContainer) {
resultsContainer.style.opacity = '1';
resultsContainer.style.pointerEvents = '';
}
}
function showWelcomeState() {
hideLoading();
if (statusBar) statusBar.classList.add('hidden');
if (resultsContainer) {
resultsContainer.innerHTML = document.getElementById('welcome-state')
? document.getElementById('welcome-state').outerHTML
: `<div class="text-center py-16 text-gray-500"><i class="fas fa-search text-4xl mb-4 text-gray-300"></i><p>Effectuez une recherche ci-dessus</p></div>`;
}
if (paginationCont) paginationCont.innerHTML = '';
}
function showErrorState() {
hideLoading();
if (statusBar) statusBar.classList.add('hidden');
if (paginationCont) paginationCont.innerHTML = '';
const q = mainInput ? mainInput.value.trim() : '';
if (resultsContainer) {
resultsContainer.innerHTML = `
<div class="text-center py-16 bg-orange-50 rounded-2xl border border-orange-100">
<div class="w-16 h-16 bg-orange-100 rounded-full flex items-center justify-center mx-auto mb-5">
<i class="fas fa-wifi text-2xl text-orange-400"></i>
</div>
<h3 class="text-lg font-semibold text-orange-700 mb-2">Connexion temporairement indisponible</h3>
<p class="text-sm text-gray-500 max-w-sm mx-auto mb-5">
La recherche en temps réel est momentanément inaccessible.
Vous pouvez relancer manuellement votre recherche.
</p>
<a href="${SEARCH_URL}${q ? '?q=' + encodeURIComponent(q) : ''}"
class="inline-flex items-center gap-2 bg-[#006633] text-white px-6 py-2.5 rounded-xl text-sm font-semibold hover:bg-[#004d26] transition shadow-md">
<i class="fas fa-redo-alt"></i> Relancer la recherche
</a>
</div>`;
}
}
function renderResults(data, query) {
hideLoading();
// Mise à jour compteur
if (statusBar) statusBar.classList.remove('hidden');
if (countLabel) {
const q = query ? `pour <em class="text-[#006633] not-italic font-black">"${escapeHtml(query)}"</em>` : '';
countLabel.innerHTML = `<span id="count-number">${data.total || 0}</span> résultat(s) ${q}`;
}
// Résultats
if (!resultsContainer) return;
if (!data.results || data.results.length === 0) {
resultsContainer.innerHTML = renderNoResults(query);
} else {
resultsContainer.innerHTML = `<div class="space-y-4">${data.results.map(r => renderResultCard(r, data.isPremium)).join('')}</div>`;
}
// Pagination
if (paginationCont) {
paginationCont.innerHTML = data.totalPages > 1
? renderPagination(data.currentPage, data.totalPages, currentParams)
: '';
}
// Scroll doux vers résultats
document.getElementById('results-section')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
// ─── Rendu côté client (fallback / AJAX) ─────────────────────────────────
const TYPE_COLORS = {
loi: 'bg-purple-100 text-purple-700 border-purple-200',
decret: 'bg-blue-100 text-blue-700 border-blue-200',
arrete: 'bg-green-100 text-green-700 border-green-200',
circulaire: 'bg-cyan-100 text-cyan-700 border-cyan-200',
avis: 'bg-sky-100 text-sky-700 border-sky-200',
marche_public: 'bg-emerald-100 text-emerald-700 border-emerald-200',
acte_foncier: 'bg-teal-100 text-teal-700 border-teal-200',
societe_rccm: 'bg-indigo-100 text-indigo-700 border-indigo-200',
association: 'bg-amber-100 text-amber-700 border-amber-200',
enchere: 'bg-orange-100 text-orange-700 border-orange-200',
};
const TYPE_ICONS = {
loi: 'fa-gavel', decret: 'fa-file-signature', arrete: 'fa-clipboard-list',
circulaire: 'fa-envelope', avis: 'fa-bell', marche_public: 'fa-briefcase',
acte_foncier: 'fa-map-marker-alt', societe_rccm: 'fa-building',
association: 'fa-handshake', enchere: 'fa-hammer',
};
const STATUT_COLORS = {
en_vigueur: 'bg-green-50 text-green-600',
abroge: 'bg-red-50 text-red-600',
modifie: 'bg-yellow-50 text-yellow-700',
suspendu: 'bg-orange-50 text-orange-600',
};
function renderResultCard(r, isPremium) {
const colorClass = TYPE_COLORS[r.type] || 'bg-gray-100 text-gray-700 border-gray-200';
const iconClass = TYPE_ICONS[r.type] || 'fa-file-alt';
const statutClr = STATUT_COLORS[r.statutJuridique] || 'bg-gray-50 text-gray-500';
const date = r.datePublication ? new Date(r.datePublication).toLocaleDateString('fr-FR') : 'N/A';
return `
<article class="bg-white border border-gray-100 rounded-2xl shadow-sm hover:shadow-md transition-all duration-200 p-5 group">
<div class="flex flex-wrap justify-between items-start gap-2 mb-3">
<div class="flex flex-wrap items-center gap-2">
<span class="inline-flex items-center gap-1.5 px-3 py-1 text-xs font-bold rounded-full border ${colorClass}">
<i class="fas ${iconClass} text-[10px]"></i> ${escapeHtml(r.typeLabel || r.type)}
</span>
${r.numero ? `<span class="text-xs text-gray-400 font-mono bg-gray-50 px-2 py-0.5 rounded">${escapeHtml(r.numero)}</span>` : ''}
${r.statutJuridique ? `<span class="text-xs px-2 py-0.5 rounded ${statutClr}">${escapeHtml(r.statutJuridique.replace('_', ' '))}</span>` : ''}
</div>
<time class="text-xs text-gray-400 flex items-center gap-1 flex-shrink-0">
<i class="far fa-calendar-alt"></i> ${date}
</time>
</div>
<h3 class="text-base font-bold text-gray-800 mb-2 group-hover:text-[#006633] transition leading-snug">
<a href="/acte/${r.id}">${escapeHtml(r.titre)}</a>
</h3>
${r.resume ? `<p class="text-gray-500 text-sm mb-3 line-clamp-2">${escapeHtml(r.resume)}</p>` : ''}
${isPremium && r.extrait ? `
<div class="mb-3 px-3 py-2 bg-amber-50 border-l-4 border-amber-400 rounded-r-lg text-xs text-gray-700">
<i class="fas fa-quote-left text-amber-400 mr-1"></i>${escapeHtml(r.extrait)}
</div>` : ''}
<div class="flex flex-wrap items-center justify-between gap-2 pt-3 border-t border-gray-50">
<div class="flex items-center gap-3 text-xs text-gray-500">
${r.institution ? `<span><i class="fas fa-landmark text-[#006633] mr-1"></i>${escapeHtml(r.institution)}</span>` : ''}
${r.territoire ? `<span><i class="fas fa-map-marker-alt text-gray-400 mr-1"></i>${escapeHtml(r.territoire)}</span>` : ''}
${isPremium && r.relevance ? `<span class="text-[#006633] font-semibold"><i class="fas fa-chart-line mr-1"></i>${Math.round(r.relevance * 100)}%</span>` : ''}
</div>
<div class="flex items-center gap-3">
<a href="/acte/${r.id}" class="text-xs text-[#006633] hover:text-[#008040] font-semibold flex items-center gap-1 transition">
<i class="fas fa-eye"></i> Consulter
</a>
${r.pdf_url ? `<a href="${escapeHtml(r.pdf_url)}" target="_blank" rel="noopener" class="text-xs text-gray-400 hover:text-gray-600 flex items-center gap-1 transition"><i class="fas fa-file-pdf"></i> PDF</a>` : ''}
</div>
</div>
</article>`;
}
function renderNoResults(query) {
const activeFilters = [];
const typeEl = document.getElementById('filter-type');
const instEl = document.getElementById('filter-institution');
const periodEl = document.getElementById('filter-period');
if (typeEl && typeEl.options[typeEl.selectedIndex]?.text && typeEl.value)
activeFilters.push(typeEl.options[typeEl.selectedIndex].text);
if (instEl && instEl.options[instEl.selectedIndex]?.text && instEl.value)
activeFilters.push(instEl.options[instEl.selectedIndex].text);
if (periodEl && periodEl.value && periodEl.value !== 'custom')
activeFilters.push('année ' + periodEl.value);
let searchDesc = '';
if (query) searchDesc += `<strong class="text-gray-800">« ${escapeHtml(query)} »</strong>`;
if (activeFilters.length) {
searchDesc += (query ? ' avec les filtres ' : 'les critères ');
searchDesc += activeFilters.map(f => `<span class="inline-flex items-center gap-1 bg-[#006633]/10 text-[#006633] text-xs font-semibold px-2 py-0.5 rounded-full">${escapeHtml(f)}</span>`).join(' ');
}
if (!searchDesc) searchDesc = 'ces critères de recherche';
return `
<div class="text-center py-16 bg-gray-50 rounded-2xl border border-gray-100">
<div class="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-5">
<i class="fas fa-file-search text-2xl text-gray-400"></i>
</div>
<h3 class="text-xl font-bold text-gray-800 mb-3">
Aucun acte trouvé pour ${searchDesc}
</h3>
<p class="text-gray-500 text-sm max-w-md mx-auto mb-6 leading-relaxed">
Votre recherche ${query ? `avec <strong class="text-gray-700">« ${escapeHtml(query)} »</strong>` : ''} ne correspond
à aucun acte publié au Journal Officiel de l'Union des Comores.
</p>
<div class="flex flex-wrap justify-center gap-3 text-xs text-gray-500 mb-6">
<span class="flex items-center gap-1.5 bg-white border border-gray-200 px-3 py-1.5 rounded-full shadow-sm">
<i class="fas fa-lightbulb text-amber-400"></i> Vérifiez l'orthographe des mots-clés
</span>
<span class="flex items-center gap-1.5 bg-white border border-gray-200 px-3 py-1.5 rounded-full shadow-sm">
<i class="fas fa-expand-alt text-blue-400"></i> Élargissez ou supprimez les filtres
</span>
<span class="flex items-center gap-1.5 bg-white border border-gray-200 px-3 py-1.5 rounded-full shadow-sm">
<i class="fas fa-hashtag text-[#006633]"></i> Essayez la référence exacte de l'acte
</span>
</div>
<div class="flex flex-wrap justify-center gap-3">
<button type="button" onclick="document.getElementById('main-search-input').value='';document.getElementById('filter-type').value='';document.getElementById('filter-institution').value='';triggerSearch(1);"
class="inline-flex items-center gap-2 border border-gray-200 text-gray-600 px-5 py-2 rounded-xl text-sm font-medium hover:border-gray-300 hover:bg-gray-50 transition">
<i class="fas fa-undo-alt text-xs"></i> Réinitialiser la recherche
</button>
<a href="/abonnement"
class="inline-flex items-center gap-2 bg-gradient-to-r from-amber-500 to-amber-400 text-white px-5 py-2 rounded-xl text-sm font-semibold hover:from-amber-600 hover:to-amber-500 transition shadow-md">
<i class="fas fa-microchip text-xs"></i> Activer Ibunas.IA Premium
</a>
</div>
</div>`;
}
function renderPagination(current, total, params) {
let pages = '';
for (let p = 1; p <= total; p++) {
if (p === current) {
pages += `<li><span class="flex items-center justify-center w-9 h-9 rounded-lg bg-[#006633] text-white font-bold text-sm">${p}</span></li>`;
} else if (p <= 3 || p >= total - 2 || Math.abs(p - current) <= 2) {
const ps = buildQueryString({ ...params, page: p });
pages += `<li><button type="button" data-page="${p}" class="pagination-btn flex items-center justify-center w-9 h-9 rounded-lg border border-gray-200 text-gray-600 hover:border-[#006633] hover:text-[#006633] transition text-sm">${p}</button></li>`;
} else if (p === 4 && current > 6) {
pages += `<li><span class="px-2 text-gray-400">…</span></li>`;
} else if (p === total - 3 && current < total - 5) {
pages += `<li><span class="px-2 text-gray-400">…</span></li>`;
}
}
const prevDisabled = current <= 1 ? 'opacity-40 pointer-events-none' : '';
const nextDisabled = current >= total ? 'opacity-40 pointer-events-none' : '';
return `
<nav class="flex justify-center mt-8" aria-label="Pagination">
<ul class="flex items-center gap-1">
<li><button type="button" data-page="${current - 1}" class="pagination-btn flex items-center justify-center w-9 h-9 rounded-lg border border-gray-200 text-gray-500 hover:border-[#006633] hover:text-[#006633] transition text-sm ${prevDisabled}"><i class="fas fa-chevron-left text-xs"></i></button></li>
${pages}
<li><button type="button" data-page="${current + 1}" class="pagination-btn flex items-center justify-center w-9 h-9 rounded-lg border border-gray-200 text-gray-500 hover:border-[#006633] hover:text-[#006633] transition text-sm ${nextDisabled}"><i class="fas fa-chevron-right text-xs"></i></button></li>
</ul>
</nav>`;
}
// Délégation pour les boutons de pagination générés dynamiquement
document.addEventListener('click', e => {
const btn = e.target.closest('.pagination-btn');
if (btn) {
const page = parseInt(btn.dataset.page);
if (page && page > 0) triggerSearch(page);
}
});
// ─── XSS helper ───────────────────────────────────────────────────────────
function escapeHtml(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
// ─── Init ─────────────────────────────────────────────────────────────────
function init() {
updateFilterChips();
// Si query présente dans l'URL au chargement, on l'affiche sans re-fetch (SSR)
// Si via navigation arrière/avant, on re-fetch
window.addEventListener('popstate', () => {
const urlParams = new URLSearchParams(window.location.search);
if (mainInput) mainInput.value = urlParams.get('q') || '';
filterSelects.forEach(el => {
el.value = urlParams.get(el.name) || '';
});
updateFilterChips();
const q = urlParams.get('q');
if (q) triggerSearch(parseInt(urlParams.get('page') || '1'));
});
}
init();
})();
</script>
{% endblock %}