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) {
console.error('Erreur lors du chargement des composants:', error)
}
finally {
localLoading.value = false
}
} }
const allowedTypeId = String(props.typeComposantId)
return baseOptions.filter((composant: any) => {
const typeId =
composant?.typeComposantId ||
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) {
console.error('Erreur lors du chargement des pièces:', error)
}
finally {
localLoading.value = false
}
} }
const allowedTypeId = String(props.typePieceId)
return baseOptions.filter((piece: any) => {
const typeId =
piece?.typePieceId ||
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) {
console.error('Erreur lors du chargement des produits:', error)
}
finally {
localLoading.value = false
}
} }
const allowedTypeId = String(props.typeProductId)
return baseOptions.filter((product) => {
const typeId =
product?.typeProductId ||
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)))
const resultTotal = extractTotal(result.data, items.length)
if (!typeComposantId) {
composants.value = enrichedItems composants.value = enrichedItems
total.value = extractTotal(result.data, items.length) total.value = resultTotal
loaded.value = true 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)))
const resultTotal = extractTotal(result.data, items.length)
// Only update global cache for unfiltered queries
if (!typePieceId) {
pieces.value = enrichedItems pieces.value = enrichedItems
total.value = extractTotal(result.data, items.length) total.value = resultTotal
loaded.value = true 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)))
const resultTotal = extractTotal(result.data, items.length)
if (!typeProductId) {
products.value = enrichedItems products.value = enrichedItems
total.value = extractTotal(result.data, items.length) total.value = resultTotal
loaded.value = true loaded.value = true
}
return { return {
success: true, success: true,
data: { data: {
items: enrichedItems, items: enrichedItems,
total: total.value, total: resultTotal,
page, page,
itemsPerPage, itemsPerPage,
}, },