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>
323 lines
9.6 KiB
TypeScript
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,
|
|
}
|
|
}
|