feat(admin) : filtres + pagination serveur sur /admin/users/sites/roles
Ajoute le filtrage par colonne et la pagination negociee via query params
sur les 3 DataTables admin existantes. Tout est cote serveur (API Platform
SearchFilter + BooleanFilter) pour scaler naturellement.
Backend :
- api_platform.yaml : scan du mapping Sites + pagination_client_items_per_page
(avec borne max 100 pour proteger contre les payloads exagerement grands).
- User : SearchFilter username (partial), rbacRoles.code (exact),
sites.name (exact) + BooleanFilter isAdmin.
- Site : SearchFilter name/city/postalCode (partial).
- Role : SearchFilter label/code (partial), permissions.code (exact).
(BooleanFilter isSystem deja present.)
Frontend :
- Composable useDataTableServerState (shared) : singleton de page/perPage/
filters avec debounce 300ms sur les filters, fetch immediat sur page/
perPage, reset page=1 au changement filter, token anti-race-condition.
- Pages admin : chaque filtre dans un slot #header-{key} (input text avec
debounce, select mono-selection pour les relations). Font-size 20px sur
les inputs de filtre.
- /admin/users : colonne Sites + filtre Sites conditionnes par
useModules().isModuleActive('sites') — preserve l'invariant "module
desactivable sans casse".
Tests : 215/215 PHPUnit (14 nouveaux filtres/pagination) + 48/48 Vitest
(8 nouveaux useDataTableServerState).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -14,16 +14,43 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Table des sites -->
|
||||
<!-- Table des sites avec filtres + pagination -->
|
||||
<MalioDataTable
|
||||
v-model:page="page"
|
||||
v-model:per-page="perPage"
|
||||
class="mt-6"
|
||||
:columns="columns"
|
||||
:items="siteItems"
|
||||
:total-items="sites.length"
|
||||
:total-items="totalItems"
|
||||
:row-clickable="canManage"
|
||||
:empty-message="t('admin.sites.noSites')"
|
||||
@row-click="onRowClick"
|
||||
>
|
||||
<template #header-name>
|
||||
<input
|
||||
v-model="filters.name"
|
||||
type="text"
|
||||
:placeholder="t('admin.sites.table.name')"
|
||||
class="w-full border-0 border-b border-black bg-transparent px-0 py-1 text-xl outline-none"
|
||||
>
|
||||
</template>
|
||||
<template #header-city>
|
||||
<input
|
||||
v-model="filters.city"
|
||||
type="text"
|
||||
:placeholder="t('admin.sites.table.city')"
|
||||
class="w-full border-0 border-b border-black bg-transparent px-0 py-1 text-xl outline-none"
|
||||
>
|
||||
</template>
|
||||
<template #header-postalCode>
|
||||
<input
|
||||
v-model="filters.postalCode"
|
||||
type="text"
|
||||
:placeholder="t('admin.sites.table.postalCode')"
|
||||
class="w-full border-0 border-b border-black bg-transparent px-0 py-1 text-xl outline-none"
|
||||
>
|
||||
</template>
|
||||
|
||||
<template #cell-color="{ item }">
|
||||
<span class="inline-flex items-center gap-2">
|
||||
<span
|
||||
@@ -68,8 +95,20 @@ const canManage = computed(() => can('sites.manage'))
|
||||
|
||||
useHead({ title: t('admin.sites.title') })
|
||||
|
||||
const sites = ref<Site[]>([])
|
||||
const loading = ref(false)
|
||||
// Etat DataTable centralise : pagination serveur + filtres debounces.
|
||||
// Les filtres name/city/postalCode sont des partiels SearchFilter cote API.
|
||||
const {
|
||||
items,
|
||||
totalItems,
|
||||
page,
|
||||
perPage,
|
||||
filters,
|
||||
reload,
|
||||
} = useDataTableServerState<Site>('/sites', {
|
||||
name: '',
|
||||
city: '',
|
||||
postalCode: '',
|
||||
})
|
||||
|
||||
const columns = [
|
||||
{ key: 'name', label: t('admin.sites.table.name') },
|
||||
@@ -83,7 +122,7 @@ const columns = [
|
||||
// `fullAddress` provient du getter computed cote backend (Site::getFullAddress)
|
||||
// au format multi-lignes — on l'aplatit en virgules pour l'affichage table.
|
||||
const siteItems = computed(() =>
|
||||
sites.value.map(site => ({
|
||||
items.value.map(site => ({
|
||||
id: site.id,
|
||||
name: site.name,
|
||||
city: site.city,
|
||||
@@ -94,7 +133,7 @@ const siteItems = computed(() =>
|
||||
)
|
||||
|
||||
function getSiteById(id: number): Site | undefined {
|
||||
return sites.value.find(s => s.id === id)
|
||||
return items.value.find(s => s.id === id)
|
||||
}
|
||||
|
||||
function onRowClick(item: Record<string, unknown>) {
|
||||
@@ -108,20 +147,6 @@ const deleteModalOpen = ref(false)
|
||||
const siteToDelete = ref<Site | null>(null)
|
||||
const deleting = ref(false)
|
||||
|
||||
async function loadSites() {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await api.get<{ member: Site[] }>(
|
||||
'/sites',
|
||||
{ itemsPerPage: 999 },
|
||||
{ toast: false },
|
||||
)
|
||||
sites.value = data.member
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openCreateDrawer() {
|
||||
selectedSite.value = null
|
||||
drawerOpen.value = true
|
||||
@@ -148,17 +173,17 @@ async function handleDelete() {
|
||||
deleteModalOpen.value = false
|
||||
siteToDelete.value = null
|
||||
drawerOpen.value = false
|
||||
await loadSites()
|
||||
reload()
|
||||
} finally {
|
||||
deleting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onSiteSaved() {
|
||||
loadSites()
|
||||
reload()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadSites()
|
||||
reload()
|
||||
})
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user