import { ref, watch } from 'vue' /** * Composable generique pour les DataTables admin avec pagination, perPage * et filtres cote serveur (API Platform + Hydra). * * Usage type dans une page admin : * * ```ts * const { items, totalItems, page, perPage, filters, loading, reload } = * useDataTableServerState('/sites', { * name: '', * city: '', * postalCode: '', * }) * ``` * * Le composable : * - traque `page`, `perPage`, et un objet `filters` reactif. * - re-fetch automatiquement a chaque changement (debounce 300ms sur * `filters` pour eviter un spam lors de la frappe clavier). * - re-fetch immediat (pas de debounce) quand `page` ou `perPage` change * — ces changements sont deja des clics user discrets. * - reinitialise `page` a 1 des qu'un filtre bouge (coherence UX : un * filtre ajuste ne doit pas laisser l'user sur "page 5 de 2 pages"). * - expose `loading` pour afficher un feedback pendant la requete. * - expose `reload()` pour forcer un fetch (ex: apres une mutation * POST/PATCH/DELETE). * * Type parameter T = la forme d'un item renvoye par l'API (le member[] * du payload Hydra est type T[]). */ export function useDataTableServerState>( endpoint: string, initialFilters: Record = {}, options: { debounceMs?: number, initialPerPage?: number } = {}, ) { const api = useApi() const debounceMs = options.debounceMs ?? 300 const initialPerPage = options.initialPerPage ?? 10 const items = ref([]) as { value: T[] } const totalItems = ref(0) const page = ref(1) const perPage = ref(initialPerPage) const filters = ref>({ ...initialFilters }) const loading = ref(false) let debounceTimer: ReturnType | null = null // Token de generation : chaque reload incremente ce compteur. Quand // une reponse arrive, on verifie que son token est toujours le plus // recent — sinon on ignore (protection anti race condition si l'user // tape vite plusieurs filtres). let requestToken = 0 /** * Construit le payload query params pour useApi.get. * Les filtres a valeur vide (chaine vide, null) sont omis pour eviter * de filtrer sur "rien" (comportement API Platform : filtre present * avec valeur vide = ne retourne aucun resultat). */ function buildQueryParams(): Record { const params: Record = { page: page.value, itemsPerPage: perPage.value, } for (const [key, value] of Object.entries(filters.value)) { if (value === '' || value === null) continue params[key] = value as string | boolean } return params } async function fetchItems(): Promise { const currentToken = ++requestToken loading.value = true try { const data = await api.get<{ member: T[], totalItems: number }>( endpoint, buildQueryParams(), { toast: false }, ) // Ignore si une requete plus recente a ete lancee entre-temps. if (currentToken !== requestToken) return // Defensive : un mock/test ou une API mal configuree peut // renvoyer undefined. On ne crash pas, on laisse les valeurs // par defaut. items.value = data?.member ?? [] totalItems.value = data?.totalItems ?? 0 } finally { if (currentToken === requestToken) { loading.value = false } } } /** * Force un refetch immediat, sans debounce. Utile apres une mutation * (POST/PATCH/DELETE) ou au mount initial. */ function reload(): void { if (debounceTimer) { clearTimeout(debounceTimer) debounceTimer = null } void fetchItems() } /** * Programme un refetch debounced. Utilise par le watcher de `filters`. */ function scheduleReload(): void { if (debounceTimer) clearTimeout(debounceTimer) debounceTimer = setTimeout(() => { debounceTimer = null void fetchItems() }, debounceMs) } // Watcher sur page/perPage : refetch immediat (pas de spam possible, // l'user clique sur un bouton pagination). watch([page, perPage], () => { reload() }) // Watcher sur filters : refetch debounced + reset page a 1 pour // eviter l'etat "filtre qui reduit le total mais user reste sur une // page inexistante". watch(filters, () => { if (page.value !== 1) { page.value = 1 // Le changement de page declenchera son propre watcher, qui // appellera reload(). Pas besoin d'en programmer un. return } scheduleReload() }, { deep: true }) return { items, totalItems, page, perPage, filters, loading, reload, } }