Files
Inventory/frontend/app/composables/useConstructeurs.ts
Matthieu f71f4c68da
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
feat(fournisseurs) : pagination serveur + search multi-champs (name/email/telephone) + filtre catégorie + tri
Backend
- Nouveau ConstructeurSearchFilter : LIKE insensible casse sur name/email + LEFT JOIN telephones.numero, accessible via ?search=
- Constructeur entity : ApiFilter ConstructeurSearchFilter, SearchFilter (categories.id exact), OrderFilter (name, email, createdAt)
- paginationMaximumItemsPerPage 200 -> 2000 (pour ConstructeurSelect et MachineDetail qui chargent l'ensemble en cache)

Frontend
- useConstructeurs : nouvelle fonction fetchConstructeursPage({ page, itemsPerPage, search, categoryId, orderField, orderDirection }) renvoyant { items, totalItems, totalPages, currentPage }
- constructeurs.vue : suppression du filtre/tri client, état page/perPage/totalItems/totalPages, watchers sur search/filter/sort qui reset page=1 et rechargent, prop pagination du DataTable câblée, recharge après create/update/delete

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 10:44:20 +02:00

289 lines
8.9 KiB
TypeScript

import { ref } from 'vue'
import { useApi } from './useApi'
import { useToast } from './useToast'
import { extractCollection, extractTotal } from '~/shared/utils/apiHelpers'
export interface ConstructeurTelephone {
'@id'?: string
id?: string
numero: string
label?: string | null
}
export interface ConstructeurCategorieRef {
'@id'?: string
id: string
name: string
}
export interface Constructeur {
'@id'?: string
id: string
name: string
email?: string | null
telephones?: ConstructeurTelephone[]
categories?: ConstructeurCategorieRef[]
createdAt?: string
updatedAt?: string
}
interface ConstructeurResult {
success: boolean
data?: Constructeur | Constructeur[]
error?: string
}
export interface ConstructeurPageOptions {
page?: number
itemsPerPage?: number
search?: string
categoryId?: string
orderField?: 'name' | 'email' | 'createdAt'
orderDirection?: 'asc' | 'desc'
}
export interface ConstructeurPageResult {
success: boolean
items: Constructeur[]
totalItems: number
totalPages: number
currentPage: number
error?: string
}
const constructeurs = ref<Constructeur[]>([])
const loading = ref(false)
const loaded = ref(false)
const uniqueConstructeurs = (items: Constructeur[] = []): Constructeur[] => {
const map = new Map<string, Constructeur>()
items.forEach((item) => {
if (item && typeof item === 'object' && typeof item.id === 'string') {
map.set(item.id, item)
}
})
return Array.from(map.values())
}
const normalizeIds = (ids: unknown[] = []): string[] => {
if (!Array.isArray(ids)) {
return []
}
return Array.from(
new Set(
ids
.map((value) => (typeof value === 'string' ? value.trim() : ''))
.filter((value) => value.length > 0),
),
)
}
const upsertConstructeurs = (items: Constructeur[] = []) => {
if (!Array.isArray(items) || !items.length) {
return
}
const merged = uniqueConstructeurs([...constructeurs.value, ...items])
constructeurs.value = merged
}
const getIndexedConstructeur = (id: string): Constructeur | null =>
constructeurs.value.find((item) => item && item.id === id) || null
const pendingFetches = new Map<string, Promise<Constructeur | null>>()
export function useConstructeurs() {
const { get, post, patch, delete: del } = useApi()
const { showSuccess, showError } = useToast()
const loadConstructeurs = async (search = '', options: { force?: boolean } = {}): Promise<ConstructeurResult> => {
if (!search && !options.force && loaded.value) {
return { success: true, data: constructeurs.value }
}
loading.value = true
try {
const params = new URLSearchParams()
params.set('itemsPerPage', '2000')
if (search) params.set('search', search)
const result = await get(`/constructeurs?${params.toString()}`)
if (result.success) {
const items = extractCollection(result.data)
constructeurs.value = uniqueConstructeurs(items)
if (!search) loaded.value = true
}
return result as ConstructeurResult
} catch (error) {
const err = error as Error
console.error('Erreur lors du chargement des fournisseurs:', error)
return { success: false, error: err.message }
} finally {
loading.value = false
}
}
const searchConstructeurs = async (search = ''): Promise<ConstructeurResult> => {
return loadConstructeurs(search)
}
const fetchConstructeursPage = async (opts: ConstructeurPageOptions = {}): Promise<ConstructeurPageResult> => {
const page = Math.max(1, opts.page ?? 1)
const itemsPerPage = Math.max(1, opts.itemsPerPage ?? 30)
loading.value = true
try {
const params = new URLSearchParams()
params.set('page', String(page))
params.set('itemsPerPage', String(itemsPerPage))
if (opts.search && opts.search.trim()) params.set('search', opts.search.trim())
if (opts.categoryId) params.set('categories.id', opts.categoryId)
if (opts.orderField) {
params.set(`order[${opts.orderField}]`, opts.orderDirection ?? 'asc')
}
const result = await get(`/constructeurs?${params.toString()}`)
if (!result.success) {
return { success: false, items: [], totalItems: 0, totalPages: 0, currentPage: page, error: result.error }
}
const items = extractCollection<Constructeur>(result.data)
const totalItems = extractTotal(result.data, items.length)
const totalPages = Math.max(1, Math.ceil(totalItems / itemsPerPage))
upsertConstructeurs(items)
return { success: true, items, totalItems, totalPages, currentPage: page }
} catch (error) {
const err = error as Error
console.error('Erreur lors du chargement de la page fournisseurs:', error)
return { success: false, items: [], totalItems: 0, totalPages: 0, currentPage: page, error: err.message }
} finally {
loading.value = false
}
}
const createConstructeur = async (data: Record<string, unknown>): Promise<ConstructeurResult> => {
loading.value = true
try {
const result = await post('/constructeurs', data)
if (result.success) {
upsertConstructeurs([result.data as Constructeur])
showSuccess(`Fournisseur "${(result.data as Constructeur).name}" créé`)
} else if (result.error) {
showError(result.error)
}
return result as ConstructeurResult
} catch (error) {
const err = error as Error
console.error('Erreur lors de la création du fournisseur:', error)
showError('Impossible de créer le fournisseur')
return { success: false, error: err.message }
} finally {
loading.value = false
}
}
const ensureConstructeurs = async (ids: unknown[] = []): Promise<Constructeur[]> => {
const normalizedIds = normalizeIds(ids)
if (!normalizedIds.length) {
return []
}
const collected: Constructeur[] = []
const missing: string[] = []
normalizedIds.forEach((id) => {
const existing = getIndexedConstructeur(id)
if (existing) {
collected.push(existing)
} else {
missing.push(id)
}
})
if (missing.length) {
const fetchTasks = missing.map((id) => {
const cached = pendingFetches.get(id)
if (cached) {
return cached
}
const task = get(`/constructeurs/${id}`)
.then((result) => {
if (result.success && result.data) {
return result.data as Constructeur
}
return null
})
.catch((error) => {
console.error('Erreur lors du chargement du fournisseur:', error)
return null
})
.finally(() => {
pendingFetches.delete(id)
})
pendingFetches.set(id, task)
return task
})
const fetched = await Promise.all(fetchTasks)
const validFetched = fetched.filter((item): item is Constructeur => item !== null && item.id !== undefined)
if (validFetched.length) {
upsertConstructeurs(validFetched)
}
}
return normalizedIds
.map((id) => getIndexedConstructeur(id))
.filter((item): item is Constructeur => item !== null)
}
const updateConstructeur = async (id: string, data: Record<string, unknown>): Promise<ConstructeurResult> => {
loading.value = true
try {
const result = await patch(`/constructeurs/${id}`, data)
if (result.success) {
upsertConstructeurs([result.data as Constructeur])
showSuccess(`Fournisseur "${(result.data as Constructeur).name}" mis à jour`)
} else if (result.error) {
showError(result.error)
}
return result as ConstructeurResult
} catch (error) {
const err = error as Error
console.error('Erreur lors de la mise à jour du fournisseur:', error)
showError('Impossible de mettre à jour le fournisseur')
return { success: false, error: err.message }
} finally {
loading.value = false
}
}
const deleteConstructeur = async (id: string): Promise<ConstructeurResult> => {
loading.value = true
try {
const result = await del(`/constructeurs/${id}`)
if (result.success) {
constructeurs.value = constructeurs.value.filter((item) => item.id !== id)
showSuccess('Fournisseur supprimé')
} else if (result.error) {
showError(result.error)
}
return result as ConstructeurResult
} catch (error) {
const err = error as Error
console.error('Erreur lors de la suppression du fournisseur:', error)
showError('Impossible de supprimer le fournisseur')
return { success: false, error: err.message }
} finally {
loading.value = false
}
}
const getConstructeurById = (id: string) => getIndexedConstructeur(id)
return {
constructeurs,
loading,
loadConstructeurs,
searchConstructeurs,
fetchConstructeursPage,
createConstructeur,
updateConstructeur,
deleteConstructeur,
getConstructeurById,
ensureConstructeurs,
}
}