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

View File

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

View File

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

View File

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

View File

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

View File

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