Files
Inventory/app/composables/useProducts.ts
Matthieu 9e303426a7 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>
2026-03-16 11:59:51 +01:00

323 lines
9.6 KiB
TypeScript

import { ref } from 'vue'
import { useToast } from './useToast'
import { useApi } from './useApi'
import { humanizeError } from '~/shared/utils/errorMessages'
import { buildConstructeurRequestPayload, uniqueConstructeurIds } from '~/shared/constructeurUtils'
import { useConstructeurs, type Constructeur } from './useConstructeurs'
import { extractRelationId, normalizeRelationIds } from '~/shared/apiRelations'
import { extractCollection } from '~/shared/utils/apiHelpers'
export interface Product {
id: string
name: string
reference?: string | null
typeProductId?: string | null
typeProduct?: { id: string; name?: string } | null
constructeurs?: Constructeur[]
constructeurIds?: string[]
supplierPrice?: number | null
createdAt?: string | null
updatedAt?: string | null
documents?: unknown[]
[key: string]: unknown
}
interface ProductListResult {
success: boolean
data?: { items: Product[]; total: number; page: number; itemsPerPage: number }
error?: string
}
interface ProductSingleResult {
success: boolean
data?: Product
error?: string
}
interface LoadProductsOptions {
search?: string
page?: number
itemsPerPage?: number
orderBy?: string
orderDir?: 'asc' | 'desc'
typeName?: string
typeProductId?: string
force?: boolean
}
const products = ref<Product[]>([])
const total = ref(0)
const loading = ref(false)
const loaded = ref(false)
const error = ref<string | null>(null)
const replaceInCache = (item: Product): boolean => {
if (!item?.id) {
return false
}
const index = products.value.findIndex((product) => product.id === item.id)
if (index === -1) {
products.value.unshift(item)
return true
}
const clone = products.value.slice()
clone[index] = item
products.value = clone
return false
}
const extractTotal = (payload: unknown, fallbackLength: number): number => {
const p = payload as Record<string, unknown> | null
if (typeof p?.totalItems === 'number') {
return p.totalItems
}
if (typeof p?.['hydra:totalItems'] === 'number') {
return p['hydra:totalItems']
}
return fallbackLength
}
export function useProducts() {
const { showError } = useToast()
const { get, post, patch, delete: del } = useApi()
const { ensureConstructeurs } = useConstructeurs()
const withResolvedConstructeurs = async (product: Product): Promise<Product> => {
if (!product || typeof product !== 'object') {
return product
}
if (!product.typeProductId) {
const typeProductId = extractRelationId(product.typeProduct)
if (typeProductId) {
product.typeProductId = typeProductId
}
}
const ids = uniqueConstructeurIds(
product.constructeurIds,
product.constructeurs,
)
const hasResolvedConstructeurs =
Array.isArray(product.constructeurs) &&
product.constructeurs.length > 0 &&
product.constructeurs.every((item) => item && typeof item === 'object')
if (ids.length && !hasResolvedConstructeurs) {
const resolved = await ensureConstructeurs(ids)
if (resolved.length) {
product.constructeurs = resolved
product.constructeurIds = ids
}
}
return product
}
const loadProducts = async (options: LoadProductsOptions = {}): Promise<ProductListResult> => {
const {
search = '',
page = 1,
itemsPerPage = 30,
orderBy = 'name',
orderDir = 'asc',
typeName,
typeProductId,
force = false,
} = options
if (!force && loaded.value && !search && !typeName && !typeProductId && page === 1) {
return {
success: true,
data: { items: products.value, total: total.value, page, itemsPerPage },
}
}
if (!typeProductId && loading.value) {
return {
success: true,
data: { items: products.value, total: total.value, page, itemsPerPage },
}
}
loading.value = true
error.value = null
try {
const params = new URLSearchParams()
params.set('itemsPerPage', String(itemsPerPage))
params.set('page', String(page))
if (search && search.trim()) {
params.set('name', search.trim())
}
if (typeName && typeName.trim()) {
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)))
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: resultTotal,
page,
itemsPerPage,
},
}
} else if (result.error) {
error.value = result.error
showError(`Impossible de charger les produits: ${result.error}`)
}
return result as ProductListResult
} catch (err) {
console.error('Erreur lors du chargement des produits:', err)
const message = humanizeError((err as Error)?.message)
error.value = message
showError(`Impossible de charger les produits.`)
return { success: false, error: message }
} finally {
loading.value = false
}
}
const createProduct = async (payload: Partial<Product>): Promise<ProductSingleResult> => {
const normalizedPayload = normalizeRelationIds(buildConstructeurRequestPayload(payload))
loading.value = true
error.value = null
try {
const result = await post('/products', normalizedPayload)
if (result.success && result.data) {
const enriched = await withResolvedConstructeurs(result.data as Product)
const added = replaceInCache(enriched)
if (added) {
total.value += 1
}
return { success: true, data: enriched }
} else if (result.error) {
error.value = result.error
showError(result.error)
}
return { success: false, error: result.error }
} catch (err) {
console.error('Erreur lors de la création du produit:', err)
const message = humanizeError((err as Error)?.message)
error.value = message
showError('Impossible de créer le produit.')
return { success: false, error: message }
} finally {
loading.value = false
}
}
const updateProduct = async (id: string, payload: Partial<Product>): Promise<ProductSingleResult> => {
const normalizedPayload = normalizeRelationIds(buildConstructeurRequestPayload(payload))
loading.value = true
error.value = null
try {
const result = await patch(`/products/${id}`, normalizedPayload)
if (result.success && result.data) {
const enriched = await withResolvedConstructeurs(result.data as Product)
replaceInCache(enriched)
return { success: true, data: enriched }
} else if (result.error) {
error.value = result.error
showError(result.error)
}
return { success: false, error: result.error }
} catch (err) {
console.error('Erreur lors de la mise à jour du produit:', err)
const message = humanizeError((err as Error)?.message)
error.value = message
showError('Impossible de mettre à jour le produit.')
return { success: false, error: message }
} finally {
loading.value = false
}
}
const deleteProduct = async (id: string): Promise<ProductSingleResult> => {
loading.value = true
error.value = null
try {
const result = await del(`/products/${id}`)
if (result.success) {
products.value = products.value.filter((product) => product.id !== id)
total.value = Math.max(0, total.value - 1)
return { success: true }
} else if (result.error) {
error.value = result.error
showError(result.error)
}
return { success: false, error: result.error }
} catch (err) {
console.error('Erreur lors de la suppression du produit:', err)
const message = humanizeError((err as Error)?.message)
error.value = message
showError('Impossible de supprimer le produit.')
return { success: false, error: message }
} finally {
loading.value = false
}
}
const getProduct = async (id: string, options: { force?: boolean } = {}): Promise<ProductSingleResult> => {
const shouldForce = !!options.force
if (!shouldForce) {
const cached = products.value.find((product) => product.id === id)
if (cached && Array.isArray(cached.constructeurs) && cached.constructeurs.length > 0) {
return { success: true, data: cached }
}
}
try {
const result = await get(`/products/${id}`)
if (result.success && result.data) {
const enriched = await withResolvedConstructeurs(result.data as Product)
replaceInCache(enriched)
return { success: true, data: enriched }
}
return { success: false, error: result.error }
} catch (err) {
console.error('Erreur lors du chargement du produit:', err)
const message = (err as Error)?.message ?? 'Erreur inconnue'
return { success: false, error: message }
}
}
const clearProductsCache = () => {
products.value = []
total.value = 0
loaded.value = false
error.value = null
}
return {
products,
total,
loading,
loaded,
error,
loadProducts,
createProduct,
updateProduct,
deleteProduct,
getProduct,
clearProductsCache,
}
}