{% extends 'base.html.twig' %}{% block title %}Journal Officiel - Archives des numéros{% endblock %}{% block meta %}<meta name="description" content="Consultez l'ensemble des archives du Journal Officiel : numéros publiés, actes officiels, textes de loi et décrets. Recherche par année, numéro, mot-clé."><meta name="robots" content="index, follow">{% endblock %}{% block body %}<div class="container mx-auto px-4 py-8"><div class="max-w-7xl mx-auto"><!-- Fil d'Ariane --><nav class="text-sm mb-6" aria-label="Breadcrumb"><ol class="flex flex-wrap items-center space-x-2 text-gray-600"><li><a href="{{ path('app_home') }}" class="hover:text-[#006633]">Accueil</a></li><li><span class="mx-2">/</span></li><li class="text-gray-900 font-medium" aria-current="page">Archives du Journal Officiel</li></ol></nav><!-- Hero --><div class="bg-gradient-to-r from-[#006633] to-[#008844] rounded-lg shadow-lg p-8 mb-8 text-white"><h1 class="text-3xl font-bold">Journal Officiel</h1><p class="text-white/80 mt-2">Consultez les archives des numéros publiés – textes officiels, décrets, arrêtés et annonces légales</p><div class="mt-4 text-sm text-white/70">Dernière mise à jour : {{ "now"|date("d/m/Y") }}</div></div><!-- Filtres + Tri --><div class="bg-white rounded-lg shadow-md p-6 mb-8"><form method="GET" id="filterForm" class="space-y-4"><div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"><div><label for="year" class="block text-sm font-medium text-gray-700 mb-1">Année</label><select name="year" id="year" class="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-[#006633]"><option value="">Toutes les années</option>{% for year in years|default([]) %}<option value="{{ year }}" {% if selectedYear|default('') == year %}selected{% endif %}>{{ year }}</option>{% endfor %}</select></div><div><label for="month" class="block text-sm font-medium text-gray-700 mb-1">Mois</label><select name="month" id="month" class="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-[#006633]"><option value="">Tous les mois</option>{% for m in 1..12 %}{% set monthName = ('01-' ~ m ~ '-2000')|date('F')|capitalize %}<option value="{{ m }}" {% if selectedMonth|default('') == m %}selected{% endif %}>{{ monthName }}</option>{% endfor %}</select></div><div><label for="numero" class="block text-sm font-medium text-gray-700 mb-1">Numéro</label><input type="text" name="numero" id="numero" value="{{ selectedNumero|default('') }}" placeholder="ex: 1234" class="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-[#006633]"></div><div><label for="keyword" class="block text-sm font-medium text-gray-700 mb-1">Mot‑clé (titre / contenu)</label><input type="text" name="keyword" id="keyword" value="{{ keyword|default('') }}" placeholder="Recherche dans les actes" class="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-[#006633]"></div></div><div class="flex flex-wrap justify-between items-center gap-3"><div class="flex flex-wrap gap-3"><button type="submit" class="bg-[#006633] text-white px-5 py-2 rounded-lg hover:bg-[#005528] transition shadow-sm flex items-center gap-1"><svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"></path></svg>Filtrer</button><a href="{{ path('app_journal_officiel') }}" class="bg-gray-200 text-gray-800 px-5 py-2 rounded-lg hover:bg-gray-300 transition">Réinitialiser</a><select id="sortSelect" class="border rounded-lg px-3 py-2 bg-white text-gray-700 focus:outline-none focus:ring-2 focus:ring-[#006633]"><option value="date_desc">Date récente → ancienne</option><option value="date_asc">Date ancienne → récente</option><option value="numero_asc">Numéro croissant</option><option value="numero_desc">Numéro décroissant</option></select></div><div class="text-sm text-gray-500" id="resultCount"><strong>{{ numeros|default([])|length }}</strong> numéro(s) affiché(s)</div></div></form></div><!-- Grille des numéros --><div id="numerosGrid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">{% for numero in numeros|default([]) %}<div class="bg-white rounded-lg shadow-md hover:shadow-lg transition overflow-hidden flex flex-col h-full"data-id="{{ numero.id }}"data-numero="{{ numero.numero }}"data-date="{{ numero.datePublication|date('Y-m-d') }}"data-description="{{ numero.description|default('')|e('html_attr') }}"data-categories="{{ numero.categories|default([])|join(',')|e('html_attr') }}"data-nb-actes="{{ numero.nbActes|default(0) }}"><div class="bg-gradient-to-r from-[#006633] to-[#008844] p-4"><div class="text-white"><div class="text-2xl font-bold flex items-center justify-between"><span>JO n°{{ numero.numero }}</span>{% if numero.estSpeciale|default(false) %}<span class="text-xs bg-yellow-200 text-yellow-800 px-2 py-1 rounded-full">Spécial</span>{% endif %}</div><div class="text-sm text-white/80 mt-1">Publié le {{ numero.datePublication|date('d/m/Y') }}</div></div></div><div class="p-4 flex-grow"><div class="text-sm text-gray-600 mb-2">📄 {{ numero.nbActes|default(0) }} acte{% if numero.nbActes|default(0) > 1 %}s{% endif %} publié(s)</div>{% if numero.description|default(false) %}<p class="text-gray-700 text-sm mb-4 line-clamp-3">{{ numero.description }}</p>{% else %}<p class="text-gray-400 text-sm italic mb-4">Aucun résumé disponible</p>{% endif %}{% if numero.categories|default([])|length > 0 %}<div class="flex flex-wrap gap-1 mb-4">{% for cat in numero.categories|slice(0,2) %}<span class="inline-block bg-gray-100 text-gray-700 text-xs px-2 py-1 rounded">{{ cat }}</span>{% endfor %}{% if numero.categories|length > 2 %}<span class="text-xs text-gray-500">+{{ numero.categories|length - 2 }}</span>{% endif %}</div>{% endif %}</div><div class="p-4 pt-0 border-t border-gray-100 mt-2"><div class="flex space-x-3">{# REMPLACEMENT : bouton au lieu de lien pour éviter la redirection #}<button type="button" class="consult-btn flex-1 text-center bg-[#006633]/10 text-[#006633] px-3 py-2 rounded hover:bg-[#006633]/20 transition text-sm font-medium">Consulter</button><button type="button" class="pdf-btn flex-1 text-center bg-gray-50 text-gray-600 px-3 py-2 rounded hover:bg-gray-100 transition text-sm flex items-center justify-center gap-1"><svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3M4 4v16a2 2 0 002 2h12a2 2 0 002-2V8l-6-6H6a2 2 0 00-2 2z"></path></svg>PDF</button></div></div></div>{% else %}<div class="col-span-full text-center text-gray-500 py-12"><div class="text-6xl mb-4">📄</div><p>Aucun numéro du Journal Officiel ne correspond à vos critères</p><a href="{{ path('app_journal_officiel') }}" class="inline-block mt-4 text-[#006633] hover:underline">Voir tous les numéros</a></div>{% endfor %}</div><!-- Pagination (inchangée) -->{% set totalPages = totalPages|default(1) %}{% set currentPage = currentPage|default(1) %}{% if totalPages > 1 %}<div class="flex flex-wrap justify-center items-center space-x-1 mt-10">{# ... la pagination existante ... #}</div>{% endif %}<!-- Section aide --><div class="mt-12 bg-gray-50 rounded-lg p-6 text-sm text-gray-700 border"><h2 class="text-lg font-semibold text-gray-800 mb-2">À propos des archives</h2><p>Le Journal Officiel contient l’ensemble des textes législatifs, réglementaires et administratifs. Vous pouvez rechercher par numéro, année ou mot‑clé. Les PDF sont téléchargeables librement.</p><div class="mt-3 flex flex-wrap gap-4"><a href="#" class="text-[#006633] hover:underline">🔍 Guide de recherche avancée</a><a href="#" class="text-[#006633] hover:underline">📧 S’abonner aux alertes</a><a href="#" class="text-[#006633] hover:underline">ℹ️ Aide & contact</a></div></div></div></div><!-- Modale --><div id="consultModal" class="modal" style="display: none;"><div class="modal-content bg-white rounded-lg shadow-xl max-w-2xl w-full mx-4 relative"><div class="bg-gradient-to-r from-[#006633] to-[#008844] p-4 rounded-t-lg text-white flex justify-between items-center"><h3 class="text-xl font-bold">Consultation du Journal Officiel</h3><button id="closeModalBtn" class="text-white hover:text-gray-200 text-2xl leading-none">×</button></div><div id="modalBody" class="p-6"><!-- contenu dynamique --></div><div class="border-t p-4 flex justify-end"><button id="modalPdfBtn" class="bg-[#006633] text-white px-4 py-2 rounded-lg hover:bg-[#005528] transition flex items-center gap-2"><svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3M4 4v16a2 2 0 002 2h12a2 2 0 002-2V8l-6-6H6a2 2 0 00-2 2z"></path></svg>Télécharger ce PDF</button></div></div></div><script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script><script>document.addEventListener('DOMContentLoaded', function() {// Élémentsconst grid = document.getElementById('numerosGrid');const filterForm = document.getElementById('filterForm');const yearSelect = document.getElementById('year');const monthSelect = document.getElementById('month');const numeroInput = document.getElementById('numero');const keywordInput = document.getElementById('keyword');const sortSelect = document.getElementById('sortSelect');const resultCountSpan = document.getElementById('resultCount');const modal = document.getElementById('consultModal');const modalBody = document.getElementById('modalBody');const closeModalBtn = document.getElementById('closeModalBtn');const modalPdfBtn = document.getElementById('modalPdfBtn');let currentModalData = null;// Fermeture modalefunction closeModal() {modal.style.display = 'none';currentModalData = null;}closeModalBtn.addEventListener('click', closeModal);window.addEventListener('click', (e) => { if(e.target === modal) closeModal(); });// Affichage modalefunction showModal(card) {const numero = card.dataset.numero;const date = card.dataset.date;const description = card.dataset.description || 'Aucune description';const nbActes = card.dataset.nbActes;const categories = card.dataset.categories ? card.dataset.categories.split(',') : [];const formattedDate = new Date(date).toLocaleDateString('fr-FR');let categoriesHtml = '';if(categories.length) {categoriesHtml = '<div class="mt-2 flex flex-wrap gap-1">' + categories.map(c => `<span class="bg-gray-100 text-gray-700 text-xs px-2 py-1 rounded">${c.trim()}</span>`).join('') + '</div>';}modalBody.innerHTML = `<h4 class="text-2xl font-bold text-[#006633]">JO n°${numero}</h4><p class="text-gray-600 mt-1">Publié le ${formattedDate}</p><div class="mt-4 text-sm text-gray-700">📄 ${nbActes} acte(s) publié(s)</div><div class="mt-4"><h5 class="font-semibold text-gray-800">Résumé</h5><p class="text-gray-600">${description}</p></div>${categoriesHtml}`;currentModalData = { numero, date: formattedDate, description, nbActes, categories };modal.style.display = 'flex';}// Génération PDFasync function generatePDF(data) {const { jsPDF } = window.jspdf;const doc = new jsPDF();doc.setFontSize(18);doc.setTextColor(0, 102, 51);doc.text(`Journal Officiel n°${data.numero}`, 20, 30);doc.setFontSize(12);doc.setTextColor(0,0,0);doc.text(`Publié le ${data.date}`, 20, 45);doc.text(`Nombre d'actes : ${data.nbActes}`, 20, 60);doc.text("Résumé :", 20, 75);const splitDesc = doc.splitTextToSize(data.description || "Aucun résumé", 170);doc.text(splitDesc, 20, 85);if(data.categories && data.categories.length) {doc.text("Catégories : " + data.categories.join(', '), 20, 85 + splitDesc.length * 7);}doc.save(`JO_${data.numero}.pdf`);}// Filtrage + trifunction filterAndSort() {const cards = Array.from(grid.querySelectorAll('.bg-white.rounded-lg.shadow-md'));const yearVal = yearSelect.value;const monthVal = monthSelect.value;const numeroVal = numeroInput.value.trim();const keywordVal = keywordInput.value.trim().toLowerCase();const sortVal = sortSelect.value;let visibleCards = cards.filter(card => {const cardNumero = card.dataset.numero;const cardDate = new Date(card.dataset.date);const cardYear = cardDate.getFullYear().toString();const cardMonth = (cardDate.getMonth() + 1).toString();const cardDescription = (card.dataset.description || '').toLowerCase();const cardCategories = (card.dataset.categories || '').toLowerCase();const searchableText = `${cardNumero} ${cardDescription} ${cardCategories}`;let match = true;if(yearVal && cardYear !== yearVal) match = false;if(match && monthVal && cardMonth !== monthVal) match = false;if(match && numeroVal && !cardNumero.includes(numeroVal)) match = false;if(match && keywordVal && !searchableText.includes(keywordVal)) match = false;return match;});// Triif(sortVal === 'date_asc') visibleCards.sort((a,b) => new Date(a.dataset.date) - new Date(b.dataset.date));else if(sortVal === 'date_desc') visibleCards.sort((a,b) => new Date(b.dataset.date) - new Date(a.dataset.date));else if(sortVal === 'numero_asc') visibleCards.sort((a,b) => parseInt(a.dataset.numero) - parseInt(b.dataset.numero));else if(sortVal === 'numero_desc') visibleCards.sort((a,b) => parseInt(b.dataset.numero) - parseInt(a.dataset.numero));// Mise à jour DOMcards.forEach(card => card.style.display = 'none');visibleCards.forEach(card => { card.style.display = ''; grid.appendChild(card); });resultCountSpan.innerHTML = `<strong>${visibleCards.length}</strong> numéro(s) affiché(s)`;}// Écouteurs filtresyearSelect.addEventListener('change', filterAndSort);monthSelect.addEventListener('change', filterAndSort);numeroInput.addEventListener('input', filterAndSort);keywordInput.addEventListener('input', filterAndSort);sortSelect.addEventListener('change', filterAndSort);filterForm.addEventListener('submit', (e) => { e.preventDefault(); filterAndSort(); });// Délégation d'événements pour les boutons (y compris ceux ajoutés dynamiquement)grid.addEventListener('click', (e) => {const btn = e.target.closest('.consult-btn');if(btn) {e.preventDefault();const card = btn.closest('.bg-white.rounded-lg.shadow-md');if(card) showModal(card);return;}const pdfBtn = e.target.closest('.pdf-btn');if(pdfBtn) {e.preventDefault();const card = pdfBtn.closest('.bg-white.rounded-lg.shadow-md');if(card) {const data = {numero: card.dataset.numero,date: new Date(card.dataset.date).toLocaleDateString('fr-FR'),description: card.dataset.description,nbActes: card.dataset.nbActes,categories: card.dataset.categories ? card.dataset.categories.split(',') : []};generatePDF(data);}}});// PDF depuis la modalemodalPdfBtn.addEventListener('click', () => { if(currentModalData) generatePDF(currentModalData); });// InitialisationfilterAndSort();});</script><style>.modal {display: none;position: fixed;top: 0;left: 0;width: 100%;height: 100%;background-color: rgba(0,0,0,0.5);z-index: 1000;justify-content: center;align-items: center;}.line-clamp-3 {display: -webkit-box;-webkit-line-clamp: 3;-webkit-box-orient: vertical;overflow: hidden;}</style>{% endblock %}