Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SCt6e8wWiYqRXLJAuKqP5i
167 lines
6.7 KiB
TypeScript
167 lines
6.7 KiB
TypeScript
/**
|
|
* useVueFiches — B11-M1. L'état de la vue fiches, branché sur l'URL.
|
|
*
|
|
* L'URL est la source de vérité (SPEC-vue-fiches §5) : l'état se LIT dans
|
|
* `route.query` (surveillée : un lien interne vers `?vue=fiches` depuis la même
|
|
* page change l'écran) et s'ÉCRIT par `router.replace` (pas d'entrée
|
|
* d'historique par clic de filtre). Seule la recherche a un tampon local, pour
|
|
* ne pas perdre de lettres pendant la frappe : elle part dans l'URL 250 ms
|
|
* après la dernière touche.
|
|
*
|
|
* La page crée l'état (elle en a besoin pour sa carte) et le passe tel quel à
|
|
* `<VueFiches :vf="vf">`. Logique pure et tests : `utils/vueFiches.ts`,
|
|
* `scripts/test-vue-fiches.mjs`.
|
|
*/
|
|
import type { MaybeRefOrGetter } from 'vue'
|
|
import type { MiniFiche } from '~/composables/useFicheAdapter'
|
|
import {
|
|
lireQuery, ecrireQuery, filtresValides, filtrerFiches, compterEtiquettes,
|
|
triEffectif, trierFiches, regrouper, ordreFiches, nbFiltresActifs,
|
|
type ConfigVueFiches, type EtatFiches, type EtatVue, type TriFiches,
|
|
} from '~/utils/vueFiches'
|
|
|
|
/** Largeur d'écran ; null avant le montage (SSR) : la CSS tranche alors seule. */
|
|
export type Largeur = 'telephone' | 'tablette' | 'ordinateur' | null
|
|
|
|
const CLE_REPLI = 'aep_vue_fiches_liste_repliee'
|
|
|
|
export function useVueFiches(
|
|
fiches: MaybeRefOrGetter<MiniFiche[]>,
|
|
config: MaybeRefOrGetter<ConfigVueFiches>,
|
|
) {
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
const cfg = computed(() => toValue(config))
|
|
|
|
// ── Lecture ──
|
|
const brut = computed(() => lireQuery(route.query, cfg.value))
|
|
|
|
const q = ref(brut.value.q)
|
|
let qEcrit = q.value
|
|
let minuteurQ: ReturnType<typeof setTimeout> | null = null
|
|
watch(() => brut.value.q, (v) => {
|
|
if (v !== qEcrit) { q.value = v; qEcrit = v }
|
|
})
|
|
|
|
const etat = computed<EtatFiches>(() => ({
|
|
...brut.value,
|
|
q: q.value,
|
|
filtres: filtresValides(brut.value.filtres, cfg.value),
|
|
}))
|
|
|
|
// ── Écriture ──
|
|
function ecrire(partiel: Partial<EtatFiches>) {
|
|
const nouvel = { ...etat.value, ...partiel }
|
|
qEcrit = nouvel.q
|
|
if (minuteurQ && partiel.q === undefined) { clearTimeout(minuteurQ); minuteurQ = null }
|
|
router.replace({ query: ecrireQuery(nouvel, cfg.value, route.query) })
|
|
}
|
|
|
|
function setQ(v: string) {
|
|
q.value = v
|
|
if (minuteurQ) clearTimeout(minuteurQ)
|
|
minuteurQ = setTimeout(() => { minuteurQ = null; ecrire({ q: v }) }, 250)
|
|
}
|
|
|
|
function avecFiltre(dim: string, valeurs: string[]) {
|
|
return { ...etat.value.filtres, [dim]: valeurs }
|
|
}
|
|
function toggle(dim: string, valeur: string) {
|
|
const vals = etat.value.filtres[dim] ?? []
|
|
ecrire({ filtres: avecFiltre(dim, vals.includes(valeur) ? vals.filter((v) => v !== valeur) : [...vals, valeur]) })
|
|
}
|
|
function ajouterFiltre(dim: string, valeur: string) {
|
|
const vals = etat.value.filtres[dim] ?? []
|
|
if (!vals.includes(valeur)) ecrire({ filtres: avecFiltre(dim, [...vals, valeur]) })
|
|
}
|
|
function effacerTout() {
|
|
q.value = ''
|
|
const vides: Record<string, string[]> = {}
|
|
for (const d of cfg.value.dimensions) vides[d.cle] = []
|
|
ecrire({ q: '', filtres: vides, sansAdresse: false })
|
|
}
|
|
const setTri = (tri: TriFiches) => ecrire({ tri })
|
|
const setGroupe = (groupe: string | null) => ecrire({ groupe })
|
|
const setSousVue = (sousVue: string) => ecrire({ sousVue })
|
|
const setSansAdresse = (sansAdresse: boolean) => ecrire({ sansAdresse })
|
|
|
|
// ── Largeur et état affiché ──
|
|
const largeur = ref<Largeur>(null)
|
|
const listeRepliee = ref(false)
|
|
|
|
onMounted(() => {
|
|
const mq768 = window.matchMedia('(min-width: 768px)')
|
|
const mq1024 = window.matchMedia('(min-width: 1024px)')
|
|
const maj = () => {
|
|
largeur.value = mq1024.matches ? 'ordinateur' : mq768.matches ? 'tablette' : 'telephone'
|
|
}
|
|
maj()
|
|
mq768.addEventListener('change', maj)
|
|
mq1024.addEventListener('change', maj)
|
|
onBeforeUnmount(() => {
|
|
mq768.removeEventListener('change', maj)
|
|
mq1024.removeEventListener('change', maj)
|
|
})
|
|
try { listeRepliee.value = sessionStorage.getItem(CLE_REPLI) === '1' } catch {}
|
|
})
|
|
|
|
/**
|
|
* État rendu. `auto` avant le montage quand l'URL ne tranche pas (ou dit
|
|
* `mixte`) : la CSS rend Mixte à partir de 1024 px et Fiches en dessous, sans
|
|
* saut à l'hydratation. Mixte n'existe pas sous 1024 px : il se lit Fiches.
|
|
*/
|
|
const vueAffichee = computed<EtatVue | 'auto'>(() => {
|
|
const etats = cfg.value.etats
|
|
const v = etat.value.vue
|
|
// `auto` rend Mixte à partir de 1024 px : seulement pour une page qui a cet état
|
|
if (largeur.value === null) return v && v !== 'mixte' ? v : etats.includes('mixte') ? 'auto' : 'fiches'
|
|
const ordinateur = largeur.value === 'ordinateur'
|
|
if (v === 'mixte' || v === null) {
|
|
if (!ordinateur || !etats.includes('mixte')) return 'fiches'
|
|
if (v === null && listeRepliee.value) return 'carte'
|
|
return 'mixte'
|
|
}
|
|
return v
|
|
})
|
|
|
|
function setVue(v: EtatVue) {
|
|
if (largeur.value === 'ordinateur' && (v === 'mixte' || v === 'carte')) {
|
|
listeRepliee.value = v === 'carte'
|
|
try { sessionStorage.setItem(CLE_REPLI, listeRepliee.value ? '1' : '0') } catch {}
|
|
}
|
|
ecrire({ vue: v })
|
|
}
|
|
|
|
// ── Résultat ──
|
|
const sansAdresseIgnore = computed(() => ({ ...etat.value, sansAdresse: false }))
|
|
const filtrees = computed(() => filtrerFiches(toValue(fiches), etat.value, cfg.value))
|
|
const tri = computed(() => triEffectif(etat.value, cfg.value))
|
|
const resultat = computed(() => trierFiches(filtrees.value, tri.value, etat.value))
|
|
const groupeDef = computed(() => cfg.value.groupes.find((g) => g.cle === etat.value.groupe) ?? null)
|
|
const groupes = computed(() => regrouper(resultat.value, groupeDef.value, cfg.value))
|
|
const ordre = computed(() => ordreFiches(groupes.value))
|
|
const compteurs = computed(() => compterEtiquettes(toValue(fiches), etat.value, cfg.value))
|
|
const idsResultat = computed(() => new Set(resultat.value.map((f) => String(f.id))))
|
|
const nbSansAdresse = computed(() =>
|
|
filtrerFiches(toValue(fiches), sansAdresseIgnore.value, cfg.value).filter((f) => !f.geoloc).length,
|
|
)
|
|
const total = computed(() => toValue(fiches).length)
|
|
const nbActifs = computed(() => nbFiltresActifs(etat.value))
|
|
|
|
function voirSansAdresse() {
|
|
ecrire({ sansAdresse: true, vue: 'fiches' })
|
|
}
|
|
|
|
/** Fiche survolée (liste ↔ épingle) ; la carte de la page s'en sert, et peut l'écrire. */
|
|
const survolId = ref<string | number | null>(null)
|
|
|
|
return reactive({
|
|
cfg, etat, q, largeur, vueAffichee, tri, resultat, groupes, ordre, compteurs,
|
|
idsResultat, nbSansAdresse, total, nbActifs, survolId,
|
|
setVue, setQ, toggle, ajouterFiltre, effacerTout, setTri, setGroupe, setSousVue,
|
|
setSansAdresse, voirSansAdresse,
|
|
})
|
|
}
|
|
|
|
export type VueFichesEtat = ReturnType<typeof useVueFiches>
|