Files
Inventory_frontend/app/composables/useProducts.ts
2026-03-24 16:57:23 +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('search', 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,
}
}