feat: ajouter le tri par nom sur les catalogues

This commit is contained in:
Matthieu
2025-10-24 15:18:24 +02:00
parent 325bdb3d6f
commit d011e58030
3 changed files with 204 additions and 56 deletions

View File

@@ -68,8 +68,8 @@ const props = withDefaults(
const selectedCategory = ref<ModelCategory>(props.category);
const searchInput = ref("");
const searchTerm = ref("");
const sort = ref<"name" | "createdAt">("createdAt");
const dir = ref<"asc" | "desc">("desc");
const sort = ref<"name" | "createdAt">("name");
const dir = ref<"asc" | "desc">("asc");
const limit = ref(20);
const offset = ref(0);

View File

@@ -26,18 +26,52 @@
</p>
</header>
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<label class="w-full sm:w-64">
<span class="text-xs font-semibold uppercase tracking-wide text-base-content/70">Recherche</span>
<input
v-model="searchTerm"
type="text"
class="input input-bordered input-sm w-full mt-1"
placeholder="Nom, référence ou catégorie…"
/>
</label>
<p class="text-xs text-base-content/50 sm:text-right">
{{ filteredComposants.length }} / {{ composantsTotal }} résultat{{ filteredComposants.length > 1 ? 's' : '' }}
<div class="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
<div class="flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center">
<label class="w-full sm:w-72">
<span class="text-xs font-semibold uppercase tracking-wide text-base-content/70">Recherche</span>
<input
v-model="searchTerm"
type="text"
class="input input-bordered input-sm w-full mt-1"
placeholder="Nom, référence ou catégorie…"
/>
</label>
<div class="flex items-center gap-2">
<label
class="text-xs font-semibold uppercase tracking-wide text-base-content/70"
for="component-catalog-sort"
>
Trier par
</label>
<select
id="component-catalog-sort"
v-model="sortField"
class="select select-bordered select-sm"
>
<option value="name">Nom</option>
<option value="createdAt">Date de création</option>
</select>
</div>
<div class="flex items-center gap-2">
<label
class="text-xs font-semibold uppercase tracking-wide text-base-content/70"
for="component-catalog-dir"
>
Ordre
</label>
<select
id="component-catalog-dir"
v-model="sortDirection"
class="select select-bordered select-sm"
>
<option value="asc">Ascendant</option>
<option value="desc">Descendant</option>
</select>
</div>
</div>
<p class="text-xs text-base-content/50 lg:text-right">
{{ visibleComposants.length }} / {{ composantsTotal }} résultat{{ visibleComposants.length > 1 ? 's' : '' }}
</p>
</div>
@@ -49,7 +83,7 @@
Aucun composant n'a encore été créé.
</p>
<p v-else-if="!filteredComposants.length" class="text-sm text-base-content/70">
<p v-else-if="!visibleComposants.length" class="text-sm text-base-content/70">
Aucun composant ne correspond à votre recherche.
</p>
@@ -64,7 +98,7 @@
</tr>
</thead>
<tbody>
<tr v-for="component in filteredComposants" :key="component.id">
<tr v-for="component in visibleComposants" :key="component.id">
<td>{{ component.name || 'Composant sans nom' }}</td>
<td>{{ component.typeComposant?.name || '—' }}</td>
<td>{{ component.reference || '—' }}</td>
@@ -108,20 +142,60 @@ const composantsList = computed(() => composants.value || [])
const composantsTotal = computed(() => composantsList.value.length)
const searchTerm = ref('')
const filteredComposants = computed(() => {
const term = searchTerm.value.trim().toLowerCase()
if (!term) {
return composantsList.value
const sortField = ref<'name' | 'createdAt'>('name')
const sortDirection = ref<'asc' | 'desc'>('asc')
const resolveComparableName = (component: Record<string, any>) => {
const toComparable = (value?: string | null) =>
(value ?? '').toString().trim().toLowerCase()
return (
toComparable(component?.name) ||
toComparable(component?.reference) ||
toComparable(component?.id)
)
}
const resolveComparableDate = (component: Record<string, any>) => {
const raw = component?.createdAt ?? component?.created_at ?? null
if (!raw) {
return 0
}
return composantsList.value.filter((component) => {
const name = (component?.name || '').toLowerCase()
const reference = (component?.reference || '').toLowerCase()
const category = (component?.typeComposant?.name || '').toLowerCase()
return (
name.includes(term) ||
reference.includes(term) ||
category.includes(term)
)
const parsed = new Date(raw).getTime()
return Number.isNaN(parsed) ? 0 : parsed
}
const visibleComposants = computed(() => {
const term = searchTerm.value.trim().toLowerCase()
const source = composantsList.value || []
const filtered = term
? source.filter((component) => {
const name = (component?.name || '').toLowerCase()
const reference = (component?.reference || '').toLowerCase()
const category = (component?.typeComposant?.name || '').toLowerCase()
return (
name.includes(term) ||
reference.includes(term) ||
category.includes(term)
)
})
: [...source]
const direction = sortDirection.value === 'asc' ? 1 : -1
return filtered.sort((a, b) => {
if (sortField.value === 'name') {
return (
resolveComparableName(a).localeCompare(
resolveComparableName(b),
'fr',
{ sensitivity: 'base' }
) * direction
)
}
return (resolveComparableDate(a) - resolveComparableDate(b)) * direction
})
})

View File

@@ -25,18 +25,52 @@
</p>
</header>
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<label class="w-full sm:w-64">
<span class="text-xs font-semibold uppercase tracking-wide text-base-content/70">Recherche</span>
<input
v-model="searchTerm"
type="text"
class="input input-bordered input-sm w-full mt-1"
placeholder="Nom, référence ou catégorie…"
/>
</label>
<p class="text-xs text-base-content/50 sm:text-right">
{{ filteredPieces.length }} / {{ piecesTotal }} résultat{{ filteredPieces.length > 1 ? 's' : '' }}
<div class="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
<div class="flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center">
<label class="w-full sm:w-72">
<span class="text-xs font-semibold uppercase tracking-wide text-base-content/70">Recherche</span>
<input
v-model="searchTerm"
type="text"
class="input input-bordered input-sm w-full mt-1"
placeholder="Nom, référence ou catégorie…"
/>
</label>
<div class="flex items-center gap-2">
<label
class="text-xs font-semibold uppercase tracking-wide text-base-content/70"
for="piece-catalog-sort"
>
Trier par
</label>
<select
id="piece-catalog-sort"
v-model="sortField"
class="select select-bordered select-sm"
>
<option value="name">Nom</option>
<option value="createdAt">Date de création</option>
</select>
</div>
<div class="flex items-center gap-2">
<label
class="text-xs font-semibold uppercase tracking-wide text-base-content/70"
for="piece-catalog-dir"
>
Ordre
</label>
<select
id="piece-catalog-dir"
v-model="sortDirection"
class="select select-bordered select-sm"
>
<option value="asc">Ascendant</option>
<option value="desc">Descendant</option>
</select>
</div>
</div>
<p class="text-xs text-base-content/50 lg:text-right">
{{ visiblePieces.length }} / {{ piecesTotal }} résultat{{ visiblePieces.length > 1 ? 's' : '' }}
</p>
</div>
@@ -48,7 +82,7 @@
Aucune pièce n'a encore été créée.
</p>
<p v-else-if="!filteredPieces.length" class="text-sm text-base-content/70">
<p v-else-if="!visiblePieces.length" class="text-sm text-base-content/70">
Aucune pièce ne correspond à votre recherche.
</p>
@@ -63,7 +97,7 @@
</tr>
</thead>
<tbody>
<tr v-for="piece in filteredPieces" :key="piece.id">
<tr v-for="piece in visiblePieces" :key="piece.id">
<td>{{ piece.name || 'Pièce sans nom' }}</td>
<td>{{ piece.typePiece?.name || '—' }}</td>
<td>{{ piece.reference || '—' }}</td>
@@ -107,20 +141,60 @@ const piecesList = computed(() => pieces.value || [])
const piecesTotal = computed(() => piecesList.value.length)
const searchTerm = ref('')
const filteredPieces = computed(() => {
const term = searchTerm.value.trim().toLowerCase()
if (!term) {
return piecesList.value
const sortField = ref<'name' | 'createdAt'>('name')
const sortDirection = ref<'asc' | 'desc'>('asc')
const resolveComparableName = (piece: Record<string, any>) => {
const normalise = (value?: string | null) =>
(value ?? '').toString().trim().toLowerCase()
return (
normalise(piece?.name) ||
normalise(piece?.reference) ||
normalise(piece?.id)
)
}
const resolveComparableDate = (piece: Record<string, any>) => {
const raw = piece?.createdAt ?? piece?.created_at ?? null
if (!raw) {
return 0
}
return piecesList.value.filter((piece) => {
const name = (piece?.name || '').toLowerCase()
const reference = (piece?.reference || '').toLowerCase()
const category = (piece?.typePiece?.name || '').toLowerCase()
return (
name.includes(term) ||
reference.includes(term) ||
category.includes(term)
)
const timestamp = new Date(raw).getTime()
return Number.isNaN(timestamp) ? 0 : timestamp
}
const visiblePieces = computed(() => {
const term = searchTerm.value.trim().toLowerCase()
const source = piecesList.value || []
const filtered = term
? source.filter((piece) => {
const name = (piece?.name || '').toLowerCase()
const reference = (piece?.reference || '').toLowerCase()
const category = (piece?.typePiece?.name || '').toLowerCase()
return (
name.includes(term) ||
reference.includes(term) ||
category.includes(term)
)
})
: [...source]
const direction = sortDirection.value === 'asc' ? 1 : -1
return filtered.sort((a, b) => {
if (sortField.value === 'name') {
return (
resolveComparableName(a).localeCompare(
resolveComparableName(b),
'fr',
{ sensitivity: 'base' }
) * direction
)
}
return (resolveComparableDate(a) - resolveComparableDate(b)) * direction
})
})