Files
Inventory/frontend/app/composables/useProducts.ts
r-dev 201485552a fix(ui) : remove legacy edit pages and history composables, unify create/edit forms
Consolidate create and edit pages into single create pages with edit mode support.
Remove obsolete catalog pages, history composables, and fix remaining code review issues.
Include migration to relink orphaned custom fields.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 11:19:50 +02:00

314 lines
9.4 KiB
TypeScript

import { ref } from 'vue'
import { useToast } from './useToast'
import { useApi } from './useApi'
import { humanizeError } from '~/shared/utils/errorMessages'
import { uniqueConstructeurIds } from '~/shared/constructeurUtils'
import { useConstructeurs, type Constructeur } from './useConstructeurs'
import { extractRelationId, normalizeRelationIds } from '~/shared/apiRelations'
import { extractCollection, extractTotal } 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
}
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 { constructeurIds, constructeurs, constructeurId, constructeur, ...cleanPayload } = payload as any
const normalizedPayload = normalizeRelationIds(cleanPayload)
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 { constructeurIds, constructeurs, constructeurId, constructeur, ...cleanPayload } = payload as any
const normalizedPayload = normalizeRelationIds(cleanPayload)
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,
}
}