Files
Inventory/app/composables/usePieces.ts
Matthieu 9e303426a7 fix(slots) : filter slot select options server-side instead of client-side
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>
2026-03-16 11:59:51 +01:00

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,
}
}