fix(aep-v1.1): bugs E2E M1 M2 M3 L1 L2 L3
M1 - Chips a11y : converti les <span> chips mobile (criteres, types, echelle, fonctions) en <button type=button> avec aria-pressed pour support clavier et lecteurs d'ecran (sidebar desktop deja en boutons). M2 - Effacer les filtres ne vide pas la search : resetFilters() reset maintenant aussi mobileSearch dans pratiques-regeneratives.vue et index.vue. M3 - FAB Soutenir overlap chip Agence : repositionne le FAB Soutenir en stack vertical avec le FAB Chatbot (right: 16px, bottom: 84px) au lieu de left: 16px, bottom: 68px. Evite l'overlap avec les chips de la bottom-sheet sur viewport intermediaire. L1 - /fiche/[id] introuvable pour pratiques : ajoute un fallback dans pages/fiche/[id].vue qui detecte si l'id correspond a une pratique regenerative et redirige vers /pratique/[id] (navigateTo replace). L2 - Label retour incorrect sur /proposer-pratique : harmonise en 'Retour aux pratiques regeneratives'. L3 - Map ne fitBounds pas apres filtre : EuropeMap et NavMap appellent maintenant fitBounds(bounds, padding 40px, maxZoom 10) quand la liste filtree contient 1 a 15 markers. Saute le tout premier rendu pour preserver la vue initiale.
This commit is contained in:
@@ -461,10 +461,12 @@ const jaugePct = computed(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ── FAB mobile soutenir ─────────────────────────────────────────────────── */
|
/* ── FAB mobile soutenir ─────────────────────────────────────────────────── */
|
||||||
|
/* Stack vertical avec le FAB Chatbot a droite (evite l'overlap avec les chips
|
||||||
|
sidebar mobile sur viewport intermediaire ~880px - bug E2E M3) */
|
||||||
.fab-soutenir {
|
.fab-soutenir {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 68px; /* au-dessus du FAB chatbot à 24px du bas + 48px de hauteur */
|
bottom: 84px; /* au-dessus du FAB chatbot a bottom-6 (24px) + 48px de hauteur + 12px gap */
|
||||||
left: 16px;
|
right: 16px;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
width: 44px;
|
width: 44px;
|
||||||
height: 44px;
|
height: 44px;
|
||||||
|
|||||||
@@ -123,6 +123,12 @@ async function initMap() {
|
|||||||
updateMarkers(L)
|
updateMarkers(L)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vue initiale (centre Europe + zoom 4) - sauvegardee pour reset
|
||||||
|
const INITIAL_CENTER: [number, number] = [50.0, 10.0]
|
||||||
|
const INITIAL_ZOOM = 4
|
||||||
|
|
||||||
|
let initialFitDone = false
|
||||||
|
|
||||||
function updateMarkers(L?: any) {
|
function updateMarkers(L?: any) {
|
||||||
if (!mapInstance || !clusterGroup) return
|
if (!mapInstance || !clusterGroup) return
|
||||||
const leaflet = L || (window as any).L
|
const leaflet = L || (window as any).L
|
||||||
@@ -158,6 +164,29 @@ function updateMarkers(L?: any) {
|
|||||||
markers.set(org.id, marker)
|
markers.set(org.id, marker)
|
||||||
clusterGroup.addLayer(marker)
|
clusterGroup.addLayer(marker)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Bug E2E L3 : recadrer la carte sur les resultats filtres
|
||||||
|
// Conditions : 1+ resultat, et au moins 1 marker hors viewport actuel.
|
||||||
|
// On evite de recadrer au tout premier rendu (laisser la vue initiale).
|
||||||
|
if (orgsWithCoords.length > 0 && initialFitDone) {
|
||||||
|
try {
|
||||||
|
const bounds = leaflet.latLngBounds(
|
||||||
|
orgsWithCoords.map((o) => [o.lat!, o.lng!])
|
||||||
|
)
|
||||||
|
// On recadre uniquement si la liste filtree est restreinte
|
||||||
|
// (evite un recadrage permanent quand toutes les fiches sont la).
|
||||||
|
if (orgsWithCoords.length <= 15) {
|
||||||
|
mapInstance.fitBounds(bounds, {
|
||||||
|
padding: [40, 40],
|
||||||
|
maxZoom: 10,
|
||||||
|
animate: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[EuropeMap] fitBounds echoue:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
initialFitDone = true
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
|
|||||||
@@ -128,6 +128,8 @@ async function initMap() {
|
|||||||
updateMarkers(L)
|
updateMarkers(L)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let initialFitDone = false
|
||||||
|
|
||||||
function updateMarkers(L?: any) {
|
function updateMarkers(L?: any) {
|
||||||
if (!mapInstance || !clusterGroup) return
|
if (!mapInstance || !clusterGroup) return
|
||||||
const leaflet = L || (window as any).L
|
const leaflet = L || (window as any).L
|
||||||
@@ -168,6 +170,25 @@ function updateMarkers(L?: any) {
|
|||||||
markers.set(org.Id, marker)
|
markers.set(org.Id, marker)
|
||||||
clusterGroup.addLayer(marker)
|
clusterGroup.addLayer(marker)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Bug E2E L3 : recadrer la carte sur les resultats filtres
|
||||||
|
if (orgsWithCoords.length > 0 && initialFitDone) {
|
||||||
|
try {
|
||||||
|
const bounds = leaflet.latLngBounds(
|
||||||
|
orgsWithCoords.map((o: any) => [o.latitude!, o.longitude!])
|
||||||
|
)
|
||||||
|
if (orgsWithCoords.length <= 15) {
|
||||||
|
mapInstance.fitBounds(bounds, {
|
||||||
|
padding: [40, 40],
|
||||||
|
maxZoom: 10,
|
||||||
|
animate: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[NavMap] fitBounds echoue:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
initialFitDone = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Réagir aux changements de filtres (liste d'orgs)
|
// Réagir aux changements de filtres (liste d'orgs)
|
||||||
|
|||||||
@@ -72,6 +72,21 @@ const { data: org, pending, error } = await useFetch<Org>(`/api/fiche/${orgId}`,
|
|||||||
key: `fiche-${orgId}`,
|
key: `fiche-${orgId}`,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ── Fallback Pratiques regeneratives (bug E2E L1) ─────────────────────
|
||||||
|
// Si /api/fiche/:id echoue, on regarde si l'id correspond a une pratique
|
||||||
|
// regenerative et on redirige automatiquement vers /pratique/:id.
|
||||||
|
if (error.value) {
|
||||||
|
try {
|
||||||
|
const pratiquesRes = await $fetch<{ list: { id: number }[] }>('/api/pratiques')
|
||||||
|
const numericId = Number(orgId)
|
||||||
|
if (!isNaN(numericId) && pratiquesRes.list?.some((p) => p.id === numericId)) {
|
||||||
|
await navigateTo(`/pratique/${numericId}`, { replace: true })
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// pas de fallback dispo, on garde l'erreur
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Commentaires — tick de rafraîchissement ───────────────────────────
|
// ── Commentaires — tick de rafraîchissement ───────────────────────────
|
||||||
const commentRefreshTick = ref(0)
|
const commentRefreshTick = ref(0)
|
||||||
|
|
||||||
|
|||||||
@@ -205,15 +205,17 @@
|
|||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<span class="text-xs font-bold uppercase tracking-wide block mb-1" style="color: var(--nav-text-muted);">ÉCHELLE</span>
|
<span class="text-xs font-bold uppercase tracking-wide block mb-1" style="color: var(--nav-text-muted);">ÉCHELLE</span>
|
||||||
<div class="flex flex-wrap gap-1">
|
<div class="flex flex-wrap gap-1">
|
||||||
<span
|
<button
|
||||||
v-for="opt in ECHELLES"
|
v-for="opt in ECHELLES"
|
||||||
:key="opt"
|
:key="opt"
|
||||||
|
type="button"
|
||||||
class="cursor-pointer px-2 py-0.5 rounded-full text-xs transition-all"
|
class="cursor-pointer px-2 py-0.5 rounded-full text-xs transition-all"
|
||||||
:style="echelle.includes(opt)
|
:style="echelle.includes(opt)
|
||||||
? 'background: var(--nav-primary); color: var(--nav-text-on-primary); font-weight: 600;'
|
? 'background: var(--nav-primary); color: var(--nav-text-on-primary); font-weight: 600;'
|
||||||
: 'background: var(--nav-bg-alt); color: var(--nav-text-muted);'"
|
: 'background: var(--nav-bg-alt); color: var(--nav-text-muted);'"
|
||||||
|
:aria-pressed="echelle.includes(opt)"
|
||||||
@click="toggleEchelle(opt)"
|
@click="toggleEchelle(opt)"
|
||||||
>{{ opt }}</span>
|
>{{ opt }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -221,15 +223,17 @@
|
|||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<span class="text-xs font-bold uppercase tracking-wide block mb-1" style="color: var(--nav-text-muted);">FONCTION</span>
|
<span class="text-xs font-bold uppercase tracking-wide block mb-1" style="color: var(--nav-text-muted);">FONCTION</span>
|
||||||
<div class="flex flex-wrap gap-1">
|
<div class="flex flex-wrap gap-1">
|
||||||
<span
|
<button
|
||||||
v-for="fn in FONCTIONS"
|
v-for="fn in FONCTIONS"
|
||||||
:key="fn"
|
:key="fn"
|
||||||
|
type="button"
|
||||||
class="cursor-pointer px-2 py-0.5 rounded-full text-xs transition-all"
|
class="cursor-pointer px-2 py-0.5 rounded-full text-xs transition-all"
|
||||||
:style="fonctions.includes(fn)
|
:style="fonctions.includes(fn)
|
||||||
? 'background: var(--nav-primary); color: var(--nav-text-on-primary); font-weight: 600;'
|
? 'background: var(--nav-primary); color: var(--nav-text-on-primary); font-weight: 600;'
|
||||||
: 'background: var(--nav-bg-alt); color: var(--nav-text-muted);'"
|
: 'background: var(--nav-bg-alt); color: var(--nav-text-muted);'"
|
||||||
|
:aria-pressed="fonctions.includes(fn)"
|
||||||
@click="toggleFonction(fn)"
|
@click="toggleFonction(fn)"
|
||||||
>{{ fn }}</span>
|
>{{ fn }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -446,6 +450,7 @@ const hasActiveFilters = computed(() =>
|
|||||||
|
|
||||||
function resetFilters() {
|
function resetFilters() {
|
||||||
search.value = ''
|
search.value = ''
|
||||||
|
mobileSearch.value = ''
|
||||||
echelle.value = []
|
echelle.value = []
|
||||||
fonctions.value = []
|
fonctions.value = []
|
||||||
territoire.value = null
|
territoire.value = null
|
||||||
|
|||||||
@@ -211,15 +211,17 @@ le réemploi de matériaux en Belgique."</p>
|
|||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<span class="text-xs font-bold uppercase tracking-wide block mb-1" style="color: var(--nav-text-muted);">CRITÈRES</span>
|
<span class="text-xs font-bold uppercase tracking-wide block mb-1" style="color: var(--nav-text-muted);">CRITÈRES</span>
|
||||||
<div class="flex flex-wrap gap-1">
|
<div class="flex flex-wrap gap-1">
|
||||||
<span
|
<button
|
||||||
v-for="c in CRITERES"
|
v-for="c in CRITERES"
|
||||||
:key="c.id"
|
:key="c.id"
|
||||||
|
type="button"
|
||||||
class="cursor-pointer px-2 py-0.5 rounded-full text-xs transition-all"
|
class="cursor-pointer px-2 py-0.5 rounded-full text-xs transition-all"
|
||||||
:style="criteres.includes(c.id)
|
:style="criteres.includes(c.id)
|
||||||
? 'background: var(--nav-primary); color: var(--nav-text-on-primary); font-weight: 600;'
|
? 'background: var(--nav-primary); color: var(--nav-text-on-primary); font-weight: 600;'
|
||||||
: 'background: var(--nav-bg-alt); color: var(--nav-text-muted);'"
|
: 'background: var(--nav-bg-alt); color: var(--nav-text-muted);'"
|
||||||
|
:aria-pressed="criteres.includes(c.id)"
|
||||||
@click="toggleCritere(c.id)"
|
@click="toggleCritere(c.id)"
|
||||||
>{{ c.label }}</span>
|
>{{ c.label }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -227,15 +229,17 @@ le réemploi de matériaux en Belgique."</p>
|
|||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<span class="text-xs font-bold uppercase tracking-wide block mb-1" style="color: var(--nav-text-muted);">TYPE</span>
|
<span class="text-xs font-bold uppercase tracking-wide block mb-1" style="color: var(--nav-text-muted);">TYPE</span>
|
||||||
<div class="flex flex-wrap gap-1">
|
<div class="flex flex-wrap gap-1">
|
||||||
<span
|
<button
|
||||||
v-for="t in TYPES_ENTITE"
|
v-for="t in TYPES_ENTITE"
|
||||||
:key="t"
|
:key="t"
|
||||||
|
type="button"
|
||||||
class="cursor-pointer px-2 py-0.5 rounded-full text-xs transition-all"
|
class="cursor-pointer px-2 py-0.5 rounded-full text-xs transition-all"
|
||||||
:style="typesEntite.includes(t)
|
:style="typesEntite.includes(t)
|
||||||
? 'background: var(--nav-primary); color: var(--nav-text-on-primary); font-weight: 600;'
|
? 'background: var(--nav-primary); color: var(--nav-text-on-primary); font-weight: 600;'
|
||||||
: 'background: var(--nav-bg-alt); color: var(--nav-text-muted);'"
|
: 'background: var(--nav-bg-alt); color: var(--nav-text-muted);'"
|
||||||
|
:aria-pressed="typesEntite.includes(t)"
|
||||||
@click="toggleType(t)"
|
@click="toggleType(t)"
|
||||||
>{{ TYPES_ENTITE_LABELS[t] ?? t }}</span>
|
>{{ TYPES_ENTITE_LABELS[t] ?? t }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -457,6 +461,7 @@ const hasActiveFilters = computed(() =>
|
|||||||
|
|
||||||
function resetFilters() {
|
function resetFilters() {
|
||||||
search.value = ''
|
search.value = ''
|
||||||
|
mobileSearch.value = ''
|
||||||
criteres.value = []
|
criteres.value = []
|
||||||
typesEntite.value = []
|
typesEntite.value = []
|
||||||
pays.value = []
|
pays.value = []
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<div class="contribuer-inner">
|
<div class="contribuer-inner">
|
||||||
<!-- Retour -->
|
<!-- Retour -->
|
||||||
<NuxtLink to="/pratiques-regeneratives" class="back-link">
|
<NuxtLink to="/pratiques-regeneratives" class="back-link">
|
||||||
← Retour à la carte
|
← Retour aux pratiques régénératives
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
|
|
||||||
<!-- En-tête -->
|
<!-- En-tête -->
|
||||||
|
|||||||
Reference in New Issue
Block a user