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>
314 lines
9.4 KiB
TypeScript
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,
|
|
}
|
|
}
|