fix(slots) : filter slot select options server-side instead of client-side

PieceSelect, ProductSelect and ComposantSelect were loading up to 200
items then filtering client-side by typeId. If the matching items were
not in the first 200, the dropdown appeared empty.

Now each select component uses API Platform filters (typePiece,
typeProduct, typeComposant) to fetch only relevant items server-side,
with local state to avoid overwriting the global catalog cache.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matthieu
2026-03-16 11:59:51 +01:00
parent d4fc0f1fee
commit 9e303426a7
6 changed files with 135 additions and 113 deletions

View File

@@ -25,7 +25,7 @@
</template>
<script setup lang="ts">
import { computed, onMounted, watch } from 'vue'
import { computed, onMounted, ref, watch } from 'vue'
import SearchSelect from '~/components/common/SearchSelect.vue'
import { useComposants } from '~/composables/useComposants'
@@ -52,43 +52,39 @@ const emit = defineEmits<{
(e: 'update:modelValue', value: string | null): void
}>()
const { composants, loading, loadComposants } = useComposants()
const { loading: globalLoading, loadComposants } = useComposants()
const composantOptions = computed(() => {
const baseOptions = Array.isArray(composants.value) ? composants.value : []
if (!props.typeComposantId) {
return baseOptions
const localComposants = ref<any[]>([])
const localLoading = ref(false)
const loading = computed(() => localLoading.value || globalLoading.value)
const composantOptions = computed(() => localComposants.value)
const loadFilteredComposants = async () => {
if (!props.typeComposantId) return
localLoading.value = true
try {
const result = await loadComposants({ typeComposantId: props.typeComposantId, itemsPerPage: 500, force: true })
if (result.success && result.data?.items) {
localComposants.value = result.data.items
}
}
const allowedTypeId = String(props.typeComposantId)
return baseOptions.filter((composant: any) => {
const typeId =
composant?.typeComposantId ||
composant?.typeComposant?.id ||
null
return typeId ? String(typeId) === allowedTypeId : false
})
})
catch (error: unknown) {
console.error('Erreur lors du chargement des composants:', error)
}
finally {
localLoading.value = false
}
}
onMounted(() => {
if (composantOptions.value.length === 0) {
loadComposants({ itemsPerPage: 200 }).catch((error: unknown) => {
console.error('Erreur lors du chargement des composants:', error)
})
}
loadFilteredComposants()
})
watch(
() => props.modelValue,
(value) => {
if (typeof value === 'string' && value) {
const exists = composantOptions.value.some((c: any) => c.id === value)
if (!exists && !loading.value) {
loadComposants({ itemsPerPage: 200, force: true }).catch((error: unknown) => {
console.error('Erreur lors du chargement des composants:', error)
})
}
}
() => props.typeComposantId,
() => {
loadFilteredComposants()
},
)

View File

@@ -25,7 +25,7 @@
</template>
<script setup lang="ts">
import { computed, onMounted, watch } from 'vue'
import { computed, onMounted, ref, watch } from 'vue'
import SearchSelect from '~/components/common/SearchSelect.vue'
import { usePieces } from '~/composables/usePieces'
@@ -52,43 +52,39 @@ const emit = defineEmits<{
(e: 'update:modelValue', value: string | null): void
}>()
const { pieces, loading, loadPieces } = usePieces()
const { loading: globalLoading, loadPieces } = usePieces()
const pieceOptions = computed(() => {
const baseOptions = Array.isArray(pieces.value) ? pieces.value : []
if (!props.typePieceId) {
return baseOptions
const localPieces = ref<any[]>([])
const localLoading = ref(false)
const loading = computed(() => localLoading.value || globalLoading.value)
const pieceOptions = computed(() => localPieces.value)
const loadFilteredPieces = async () => {
if (!props.typePieceId) return
localLoading.value = true
try {
const result = await loadPieces({ typePieceId: props.typePieceId, itemsPerPage: 500, force: true })
if (result.success && result.data?.items) {
localPieces.value = result.data.items
}
}
const allowedTypeId = String(props.typePieceId)
return baseOptions.filter((piece: any) => {
const typeId =
piece?.typePieceId ||
piece?.typePiece?.id ||
null
return typeId ? String(typeId) === allowedTypeId : false
})
})
catch (error: unknown) {
console.error('Erreur lors du chargement des pièces:', error)
}
finally {
localLoading.value = false
}
}
onMounted(() => {
if (pieceOptions.value.length === 0) {
loadPieces({ itemsPerPage: 200 }).catch((error: unknown) => {
console.error('Erreur lors du chargement des pièces:', error)
})
}
loadFilteredPieces()
})
watch(
() => props.modelValue,
(value) => {
if (typeof value === 'string' && value) {
const exists = pieceOptions.value.some((piece: any) => piece.id === value)
if (!exists && !loading.value) {
loadPieces({ itemsPerPage: 200, force: true }).catch((error: unknown) => {
console.error('Erreur lors du chargement des pièces:', error)
})
}
}
() => props.typePieceId,
() => {
loadFilteredPieces()
},
)

View File

@@ -25,7 +25,7 @@
</template>
<script setup lang="ts">
import { computed, onMounted, watch } from 'vue'
import { computed, onMounted, ref, watch } from 'vue'
import SearchSelect from '~/components/common/SearchSelect.vue'
import { useProducts } from '~/composables/useProducts'
@@ -52,43 +52,39 @@ const emit = defineEmits<{
(e: 'update:modelValue', value: string | null): void
}>()
const { products, loading, loadProducts } = useProducts()
const { loading: globalLoading, loadProducts } = useProducts()
const productOptions = computed(() => {
const baseOptions = Array.isArray(products.value) ? products.value : []
if (!props.typeProductId) {
return baseOptions
const localProducts = ref<any[]>([])
const localLoading = ref(false)
const loading = computed(() => localLoading.value || globalLoading.value)
const productOptions = computed(() => localProducts.value)
const loadFilteredProducts = async () => {
if (!props.typeProductId) return
localLoading.value = true
try {
const result = await loadProducts({ typeProductId: props.typeProductId, itemsPerPage: 500, force: true })
if (result.success && result.data?.items) {
localProducts.value = result.data.items
}
}
const allowedTypeId = String(props.typeProductId)
return baseOptions.filter((product) => {
const typeId =
product?.typeProductId ||
product?.typeProduct?.id ||
null
return typeId ? String(typeId) === allowedTypeId : false
})
})
catch (error: unknown) {
console.error('Erreur lors du chargement des produits:', error)
}
finally {
localLoading.value = false
}
}
onMounted(() => {
if (productOptions.value.length === 0) {
loadProducts().catch((error) => {
console.error('Erreur lors du chargement des produits:', error)
})
}
loadFilteredProducts()
})
watch(
() => props.modelValue,
(value) => {
if (typeof value === 'string' && value) {
const exists = productOptions.value.some((product) => product.id === value)
if (!exists && !loading.value) {
loadProducts({ force: true }).catch((error) => {
console.error('Erreur lors du chargement des produits:', error)
})
}
}
() => props.typeProductId,
() => {
loadFilteredProducts()
},
)

View File

@@ -42,6 +42,7 @@ interface LoadComposantsOptions {
orderBy?: string
orderDir?: 'asc' | 'desc'
typeName?: string
typeComposantId?: string
force?: boolean
}
@@ -109,17 +110,18 @@ export function useComposants() {
orderBy = 'name',
orderDir = 'asc',
typeName,
typeComposantId,
force = false,
} = options
if (!force && loaded.value && !search && !typeName && page === 1) {
if (!force && loaded.value && !search && !typeName && !typeComposantId && page === 1) {
return {
success: true,
data: { items: composants.value, total: total.value, page, itemsPerPage },
}
}
if (loading.value) {
if (!typeComposantId && loading.value) {
return {
success: true,
data: { items: composants.value, total: total.value, page, itemsPerPage },
@@ -128,7 +130,6 @@ export function useComposants() {
loading.value = true
try {
const params = new URLSearchParams()
params.set('itemsPerPage', String(itemsPerPage))
params.set('page', String(page))
@@ -141,20 +142,29 @@ export function useComposants() {
params.set('typeComposant.name', typeName.trim())
}
if (typeComposantId) {
params.set('typeComposant', typeComposantId)
}
params.set(`order[${orderBy}]`, orderDir)
const result = await get(`/composants?${params.toString()}`)
if (result.success) {
const items = extractCollection(result.data)
const enrichedItems = await Promise.all(items.map((item) => withResolvedConstructeurs(item)))
composants.value = enrichedItems
total.value = extractTotal(result.data, items.length)
loaded.value = true
const resultTotal = extractTotal(result.data, items.length)
if (!typeComposantId) {
composants.value = enrichedItems
total.value = resultTotal
loaded.value = true
}
return {
success: true,
data: {
items: enrichedItems,
total: total.value,
total: resultTotal,
page,
itemsPerPage,
},

View File

@@ -43,6 +43,7 @@ interface LoadPiecesOptions {
orderBy?: string
orderDir?: 'asc' | 'desc'
typeName?: string
typePieceId?: string
force?: boolean
}
@@ -119,17 +120,20 @@ export function usePieces() {
orderBy = 'name',
orderDir = 'asc',
typeName,
typePieceId,
force = false,
} = options
if (!force && loaded.value && !search && !typeName && page === 1) {
// Only use cache for unfiltered full-catalog loads
if (!force && loaded.value && !search && !typeName && !typePieceId && page === 1) {
return {
success: true,
data: { items: pieces.value, total: total.value, page, itemsPerPage },
}
}
if (loading.value) {
// For filtered queries, don't block on global loading state
if (!typePieceId && loading.value) {
return {
success: true,
data: { items: pieces.value, total: total.value, page, itemsPerPage },
@@ -138,7 +142,6 @@ export function usePieces() {
loading.value = true
try {
const params = new URLSearchParams()
params.set('itemsPerPage', String(itemsPerPage))
params.set('page', String(page))
@@ -151,20 +154,30 @@ export function usePieces() {
params.set('typePiece.name', typeName.trim())
}
if (typePieceId) {
params.set('typePiece', typePieceId)
}
params.set(`order[${orderBy}]`, orderDir)
const result = await get(`/pieces?${params.toString()}`)
if (result.success) {
const items = extractCollection(result.data)
const enrichedItems = await Promise.all(items.map((item) => withResolvedConstructeurs(item)))
pieces.value = enrichedItems
total.value = extractTotal(result.data, items.length)
loaded.value = true
const resultTotal = extractTotal(result.data, items.length)
// Only update global cache for unfiltered queries
if (!typePieceId) {
pieces.value = enrichedItems
total.value = resultTotal
loaded.value = true
}
return {
success: true,
data: {
items: enrichedItems,
total: total.value,
total: resultTotal,
page,
itemsPerPage,
},

View File

@@ -41,6 +41,7 @@ interface LoadProductsOptions {
orderBy?: string
orderDir?: 'asc' | 'desc'
typeName?: string
typeProductId?: string
force?: boolean
}
@@ -118,17 +119,18 @@ export function useProducts() {
orderBy = 'name',
orderDir = 'asc',
typeName,
typeProductId,
force = false,
} = options
if (!force && loaded.value && !search && !typeName && page === 1) {
if (!force && loaded.value && !search && !typeName && !typeProductId && page === 1) {
return {
success: true,
data: { items: products.value, total: total.value, page, itemsPerPage },
}
}
if (loading.value) {
if (!typeProductId && loading.value) {
return {
success: true,
data: { items: products.value, total: total.value, page, itemsPerPage },
@@ -150,20 +152,29 @@ export function useProducts() {
params.set('typeProduct.name', typeName.trim())
}
if (typeProductId) {
params.set('typeProduct', typeProductId)
}
params.set(`order[${orderBy}]`, orderDir)
const result = await get(`/products?${params.toString()}`)
if (result.success) {
const items = extractCollection(result.data)
const enrichedItems = await Promise.all(items.map((item) => withResolvedConstructeurs(item)))
products.value = enrichedItems
total.value = extractTotal(result.data, items.length)
loaded.value = true
const resultTotal = extractTotal(result.data, items.length)
if (!typeProductId) {
products.value = enrichedItems
total.value = resultTotal
loaded.value = true
}
return {
success: true,
data: {
items: enrichedItems,
total: total.value,
total: resultTotal,
page,
itemsPerPage,
},