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>
288 lines
8.7 KiB
TypeScript
288 lines
8.7 KiB
TypeScript
import { ref } from 'vue'
|
|
import { useToast } from './useToast'
|
|
import { useApi } from './useApi'
|
|
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 Piece {
|
|
id: string
|
|
name: string
|
|
reference?: string | null
|
|
description?: string | null
|
|
typePieceId?: string | null
|
|
typePiece?: { id: string; name?: string } | null
|
|
productId?: string | null
|
|
productIds?: string[]
|
|
product?: { id: string; name?: string } | null
|
|
constructeurs?: Constructeur[]
|
|
constructeurIds?: string[]
|
|
documents?: unknown[]
|
|
createdAt?: string | null
|
|
updatedAt?: string | null
|
|
[key: string]: unknown
|
|
}
|
|
|
|
interface PieceListResult {
|
|
success: boolean
|
|
data?: { items: Piece[]; total: number; page: number; itemsPerPage: number }
|
|
error?: string
|
|
}
|
|
|
|
interface PieceSingleResult {
|
|
success: boolean
|
|
data?: Piece
|
|
error?: string
|
|
}
|
|
|
|
interface LoadPiecesOptions {
|
|
search?: string
|
|
page?: number
|
|
itemsPerPage?: number
|
|
orderBy?: string
|
|
orderDir?: 'asc' | 'desc'
|
|
typeName?: string
|
|
typePieceId?: string
|
|
force?: boolean
|
|
}
|
|
|
|
const pieces = ref<Piece[]>([])
|
|
const total = ref(0)
|
|
const loading = ref(false)
|
|
const loaded = ref(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 usePieces() {
|
|
const { showSuccess } = useToast()
|
|
const { get, post, patch, delete: del } = useApi()
|
|
const { ensureConstructeurs } = useConstructeurs()
|
|
|
|
const withResolvedConstructeurs = async (piece: Piece): Promise<Piece> => {
|
|
if (!piece || typeof piece !== 'object') {
|
|
return piece
|
|
}
|
|
if (!piece.typePieceId) {
|
|
const typePieceId = extractRelationId(piece.typePiece)
|
|
if (typePieceId) {
|
|
piece.typePieceId = typePieceId
|
|
}
|
|
}
|
|
if (!piece.productId) {
|
|
const productId = extractRelationId(piece.product)
|
|
if (productId) {
|
|
piece.productId = productId
|
|
}
|
|
}
|
|
const productIds = Array.isArray(piece.productIds) ? piece.productIds.filter(Boolean) : []
|
|
if (productIds.length === 0 && piece.productId) {
|
|
piece.productIds = [piece.productId]
|
|
} else if (productIds.length > 0) {
|
|
piece.productIds = productIds.map((id) => String(id))
|
|
if (!piece.productId) {
|
|
piece.productId = piece.productIds[0] || null
|
|
}
|
|
}
|
|
const ids = uniqueConstructeurIds(
|
|
piece.constructeurIds,
|
|
piece.constructeurs,
|
|
)
|
|
const hasResolvedConstructeurs =
|
|
Array.isArray(piece.constructeurs) &&
|
|
piece.constructeurs.length > 0 &&
|
|
piece.constructeurs.every((item) => item && typeof item === 'object')
|
|
|
|
if (ids.length && !hasResolvedConstructeurs) {
|
|
const resolved = await ensureConstructeurs(ids)
|
|
if (resolved.length) {
|
|
piece.constructeurs = resolved
|
|
piece.constructeurIds = ids
|
|
}
|
|
}
|
|
return piece
|
|
}
|
|
|
|
const loadPieces = async (options: LoadPiecesOptions = {}): Promise<PieceListResult> => {
|
|
const {
|
|
search = '',
|
|
page = 1,
|
|
itemsPerPage = 30,
|
|
orderBy = 'name',
|
|
orderDir = 'asc',
|
|
typeName,
|
|
typePieceId,
|
|
force = false,
|
|
} = options
|
|
|
|
// Only use cache for unfiltered full-catalog loads
|
|
if (!force && loaded.value && !search && !typeName && !typePieceId && page === 1) {
|
|
return {
|
|
success: true,
|
|
data: { items: pieces.value, total: total.value, page, itemsPerPage },
|
|
}
|
|
}
|
|
|
|
// For filtered queries, don't block on global loading state
|
|
if (!typePieceId && loading.value) {
|
|
return {
|
|
success: true,
|
|
data: { items: pieces.value, total: total.value, page, itemsPerPage },
|
|
}
|
|
}
|
|
|
|
loading.value = true
|
|
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('typePiece.name', typeName.trim())
|
|
}
|
|
|
|
if (typePieceId) {
|
|
params.set('typePiece', typePieceId)
|
|
}
|
|
|
|
params.set(`order[${orderBy}]`, orderDir)
|
|
|
|
const result = await get(`/pieces?${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)
|
|
|
|
// Only update global cache for unfiltered queries
|
|
if (!typePieceId) {
|
|
pieces.value = enrichedItems
|
|
total.value = resultTotal
|
|
loaded.value = true
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
data: {
|
|
items: enrichedItems,
|
|
total: resultTotal,
|
|
page,
|
|
itemsPerPage,
|
|
},
|
|
}
|
|
}
|
|
return result as PieceListResult
|
|
} catch (error) {
|
|
console.error('Erreur lors du chargement des pièces:', error)
|
|
return { success: false, error: (error as Error).message }
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
const createPiece = async (pieceData: Partial<Piece>): Promise<PieceSingleResult> => {
|
|
loading.value = true
|
|
try {
|
|
const normalizedPayload = normalizeRelationIds(buildConstructeurRequestPayload(pieceData))
|
|
const result = await post('/pieces', normalizedPayload)
|
|
if (result.success && result.data) {
|
|
const enriched = await withResolvedConstructeurs(result.data as Piece)
|
|
pieces.value.unshift(enriched)
|
|
total.value += 1
|
|
const definition = (pieceData as Record<string, unknown>)?.definition as Record<string, unknown> | undefined
|
|
const displayName =
|
|
(result.data as Piece)?.name ||
|
|
(definition?.name as string | undefined) ||
|
|
pieceData?.name ||
|
|
'Pièce'
|
|
showSuccess(`Pièce "${displayName}" créée avec succès`)
|
|
return { success: true, data: enriched }
|
|
}
|
|
return { success: false, error: result.error }
|
|
} catch (error) {
|
|
console.error('Erreur lors de la création de la pièce:', error)
|
|
return { success: false, error: (error as Error).message }
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
const updatePieceData = async (id: string, pieceData: Partial<Piece>): Promise<PieceSingleResult> => {
|
|
loading.value = true
|
|
try {
|
|
const normalizedPayload = normalizeRelationIds(buildConstructeurRequestPayload(pieceData))
|
|
const result = await patch(`/pieces/${id}`, normalizedPayload)
|
|
if (result.success && result.data) {
|
|
const updated = await withResolvedConstructeurs(result.data as Piece)
|
|
const index = pieces.value.findIndex((piece) => piece.id === id)
|
|
if (index !== -1) {
|
|
pieces.value[index] = updated
|
|
}
|
|
showSuccess(`Pièce "${updated?.name || pieceData.name || ''}" mise à jour avec succès`)
|
|
return { success: true, data: updated }
|
|
}
|
|
return { success: false, error: result.error }
|
|
} catch (error) {
|
|
console.error('Erreur lors de la mise à jour de la pièce:', error)
|
|
return { success: false, error: (error as Error).message }
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
const deletePiece = async (id: string): Promise<PieceSingleResult> => {
|
|
loading.value = true
|
|
try {
|
|
const result = await del(`/pieces/${id}`)
|
|
if (result.success) {
|
|
const deletedPiece = pieces.value.find((piece) => piece.id === id)
|
|
pieces.value = pieces.value.filter((piece) => piece.id !== id)
|
|
total.value = Math.max(0, total.value - 1)
|
|
showSuccess(`Pièce "${deletedPiece?.name || 'inconnu'}" supprimée avec succès`)
|
|
return { success: true }
|
|
}
|
|
return { success: false, error: result.error }
|
|
} catch (error) {
|
|
console.error('Erreur lors de la suppression de la pièce:', error)
|
|
return { success: false, error: (error as Error).message }
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
const getPieces = () => pieces.value
|
|
const isLoading = () => loading.value
|
|
|
|
const clearPiecesCache = () => {
|
|
pieces.value = []
|
|
total.value = 0
|
|
loaded.value = false
|
|
}
|
|
|
|
return {
|
|
pieces,
|
|
total,
|
|
loading,
|
|
loaded,
|
|
loadPieces,
|
|
createPiece,
|
|
updatePiece: updatePieceData,
|
|
deletePiece,
|
|
getPieces,
|
|
isLoading,
|
|
clearPiecesCache,
|
|
}
|
|
}
|