refactor(front): extract shared utils and rewire pages

This commit is contained in:
Matthieu
2026-02-06 17:16:16 +01:00
parent 1fbd1d1b2e
commit 9ee348fff0
36 changed files with 1686 additions and 2194 deletions

View File

@@ -1,14 +1,34 @@
import { ref } from 'vue'
import { useToast } from './useToast'
import { listModelTypes, createModelType, updateModelType, deleteModelType } from '~/services/modelTypes'
import { listModelTypes, createModelType, updateModelType, deleteModelType, type ModelType } from '~/services/modelTypes'
import type { ComponentModelStructure } from '~/shared/types/inventory'
const componentTypes = ref([])
export interface ComponentType extends ModelType {
structure: ComponentModelStructure | null
description?: string | null
}
interface ComponentTypePayload {
name: string
code?: string
description?: string | null
notes?: string | null
structure?: ComponentModelStructure | null
}
interface ComponentTypeResult {
success: boolean
data?: ComponentType | ComponentType[]
error?: string
}
const componentTypes = ref<ComponentType[]>([])
const loadingComponentTypes = ref(false)
export function useComponentTypes () {
export function useComponentTypes() {
const { showSuccess, showError } = useToast()
const generateCodeFromName = (name) => {
const generateCodeFromName = (name: string): string => {
return (name || '')
.normalize('NFD')
.replace(/[\u0300-\u036F]/g, '')
@@ -18,24 +38,26 @@ export function useComponentTypes () {
.replace(/-+/g, '-') || 'type'
}
const loadComponentTypes = async () => {
const loadComponentTypes = async (): Promise<ComponentTypeResult> => {
loadingComponentTypes.value = true
try {
const data = await listModelTypes({
category: 'COMPONENT',
sort: 'name',
dir: 'asc',
limit: 200
limit: 200,
})
componentTypes.value = data.items.map(item => ({
componentTypes.value = data.items.map((item) => ({
...item,
description: item.description ?? item.notes ?? null
structure: item.structure as ComponentModelStructure | null,
description: item.description ?? item.notes ?? null,
}))
return { success: true, data: componentTypes.value }
} catch (error) {
const message = error?.message || 'Erreur inconnue'
const err = error as Error & { message?: string }
const message = err?.message || 'Erreur inconnue'
showError(`Impossible de charger les types de composant: ${message}`)
return { success: false, error: message }
} finally {
@@ -43,21 +65,22 @@ export function useComponentTypes () {
}
}
const createComponentType = async (payload) => {
const createComponentType = async (payload: ComponentTypePayload): Promise<ComponentTypeResult> => {
loadingComponentTypes.value = true
try {
const data = await createModelType({
name: payload.name,
code: payload.code || generateCodeFromName(payload.name),
category: 'COMPONENT',
notes: payload.description ?? payload.notes,
description: payload.description ?? null,
structure: payload.structure
notes: payload.description ?? payload.notes ?? undefined,
description: payload.description ?? undefined,
structure: payload.structure ?? undefined,
})
const normalized = {
const normalized: ComponentType = {
...data,
description: data.description ?? data.notes ?? null
structure: data.structure as ComponentModelStructure | null,
description: data.description ?? data.notes ?? null,
}
componentTypes.value.push(normalized)
@@ -65,7 +88,8 @@ export function useComponentTypes () {
return { success: true, data: normalized }
} catch (error) {
const message = error?.data?.message || error?.message || 'Erreur inconnue'
const err = error as Error & { data?: { message?: string }; message?: string }
const message = err?.data?.message || err?.message || 'Erreur inconnue'
showError(`Erreur lors de la création du type de composant: ${message}`)
return { success: false, error: message }
} finally {
@@ -73,34 +97,33 @@ export function useComponentTypes () {
}
}
const updateComponentType = async (id, payload) => {
const updateComponentType = async (id: string, payload: ComponentTypePayload): Promise<ComponentTypeResult> => {
loadingComponentTypes.value = true
try {
const data = await updateModelType(id, {
name: payload.name,
description: payload.description,
notes: payload.notes,
description: payload.description ?? undefined,
notes: payload.notes ?? undefined,
code: payload.code,
structure: payload.structure
structure: payload.structure ?? undefined,
})
const normalized = {
const normalized: ComponentType = {
...data,
description: data.description ?? data.notes ?? null
structure: data.structure as ComponentModelStructure | null,
description: data.description ?? data.notes ?? null,
}
const index = componentTypes.value.findIndex(type => type.id === id)
const index = componentTypes.value.findIndex((type) => type.id === id)
if (index !== -1) {
componentTypes.value[index] = normalized
}
showSuccess(`Type de composant "${data.name}" mis à jour`)
return {
success: true,
data: normalized
}
return { success: true, data: normalized }
} catch (error) {
const message = error?.data?.message || error?.message || 'Erreur inconnue'
const err = error as Error & { data?: { message?: string }; message?: string }
const message = err?.data?.message || err?.message || 'Erreur inconnue'
showError(`Erreur lors de la mise à jour du type de composant: ${message}`)
return { success: false, error: message }
} finally {
@@ -108,15 +131,16 @@ export function useComponentTypes () {
}
}
const deleteComponentType = async (id) => {
const deleteComponentType = async (id: string): Promise<ComponentTypeResult> => {
loadingComponentTypes.value = true
try {
await deleteModelType(id)
componentTypes.value = componentTypes.value.filter(type => type.id !== id)
componentTypes.value = componentTypes.value.filter((type) => type.id !== id)
showSuccess('Type de composant supprimé')
return { success: true }
} catch (error) {
const message = error?.data?.message || error?.message || 'Erreur inconnue'
const err = error as Error & { data?: { message?: string }; message?: string }
const message = err?.data?.message || err?.message || 'Erreur inconnue'
showError(`Erreur lors de la suppression du type de composant: ${message}`)
return { success: false, error: message }
} finally {
@@ -135,6 +159,6 @@ export function useComponentTypes () {
updateComponentType,
deleteComponentType,
getComponentTypes,
isComponentTypeLoading
isComponentTypeLoading,
}
}

View File

@@ -2,45 +2,83 @@ import { ref } from 'vue'
import { useToast } from './useToast'
import { useApi } from './useApi'
import { buildConstructeurRequestPayload, uniqueConstructeurIds } from '~/shared/constructeurUtils'
import { useConstructeurs } from './useConstructeurs'
import { useConstructeurs, type Constructeur } from './useConstructeurs'
import { extractRelationId, normalizeRelationIds } from '~/shared/apiRelations'
const composants = ref([])
export interface Composant {
id: string
name: string
reference?: string | null
typeComposantId?: string | null
typeComposant?: { id: string; name?: string } | null
productId?: string | null
product?: { id: string; name?: string } | null
constructeurs?: Constructeur[]
constructeurIds?: string[]
documents?: unknown[]
createdAt?: string | null
updatedAt?: string | null
[key: string]: unknown
}
interface ComposantListResult {
success: boolean
data?: { items: Composant[]; total: number; page: number; itemsPerPage: number }
error?: string
}
interface ComposantSingleResult {
success: boolean
data?: Composant
error?: string
}
interface LoadComposantsOptions {
search?: string
page?: number
itemsPerPage?: number
orderBy?: string
orderDir?: 'asc' | 'desc'
}
const composants = ref<Composant[]>([])
const total = ref(0)
const loading = ref(false)
const extractCollection = (payload) => {
const extractCollection = (payload: unknown): Composant[] => {
if (Array.isArray(payload)) {
return payload
return payload as Composant[]
}
if (Array.isArray(payload?.member)) {
return payload.member
const p = payload as Record<string, unknown> | null
if (Array.isArray(p?.member)) {
return p.member as Composant[]
}
if (Array.isArray(payload?.['hydra:member'])) {
return payload['hydra:member']
if (Array.isArray(p?.['hydra:member'])) {
return p['hydra:member'] as Composant[]
}
if (Array.isArray(payload?.data)) {
return payload.data
if (Array.isArray(p?.data)) {
return p.data as Composant[]
}
return []
}
const extractTotal = (payload, fallbackLength) => {
if (typeof payload?.totalItems === 'number') {
return payload.totalItems
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 payload?.['hydra:totalItems'] === 'number') {
return payload['hydra:totalItems']
if (typeof p?.['hydra:totalItems'] === 'number') {
return p['hydra:totalItems']
}
return fallbackLength
}
export function useComposants () {
const { showSuccess, showError, showInfo } = useToast()
export function useComposants() {
const { showSuccess } = useToast()
const { get, post, patch, delete: del } = useApi()
const { ensureConstructeurs } = useConstructeurs()
const withResolvedConstructeurs = async (composant) => {
const withResolvedConstructeurs = async (composant: Composant): Promise<Composant> => {
if (!composant || typeof composant !== 'object') {
return composant
}
@@ -59,12 +97,11 @@ export function useComposants () {
const ids = uniqueConstructeurIds(
composant.constructeurIds,
composant.constructeurs,
composant.constructeur,
)
const hasResolvedConstructeurs =
Array.isArray(composant.constructeurs)
&& composant.constructeurs.length > 0
&& composant.constructeurs.every((item) => item && typeof item === 'object')
Array.isArray(composant.constructeurs) &&
composant.constructeurs.length > 0 &&
composant.constructeurs.every((item) => item && typeof item === 'object')
if (ids.length && !hasResolvedConstructeurs) {
const resolved = await ensureConstructeurs(ids)
@@ -76,16 +113,7 @@ export function useComposants () {
return composant
}
/**
* Load composants with pagination and search support
* @param {Object} options - Query options
* @param {string} [options.search] - Search term for name/reference
* @param {number} [options.page=1] - Current page (1-based)
* @param {number} [options.itemsPerPage=30] - Items per page
* @param {string} [options.orderBy='name'] - Field to order by
* @param {string} [options.orderDir='asc'] - Order direction (asc/desc)
*/
const loadComposants = async (options = {}) => {
const loadComposants = async (options: LoadComposantsOptions = {}): Promise<ComposantListResult> => {
loading.value = true
try {
const {
@@ -93,7 +121,7 @@ export function useComposants () {
page = 1,
itemsPerPage = 30,
orderBy = 'name',
orderDir = 'asc'
orderDir = 'asc',
} = options
const params = new URLSearchParams()
@@ -118,79 +146,84 @@ export function useComposants () {
items: enrichedItems,
total: total.value,
page,
itemsPerPage
}
itemsPerPage,
},
}
}
return result
return result as ComposantListResult
} catch (error) {
console.error('Erreur lors du chargement des composants:', error)
return { success: false, error: error.message }
return { success: false, error: (error as Error).message }
} finally {
loading.value = false
}
}
const createComposant = async (composantData) => {
const createComposant = async (composantData: Partial<Composant>): Promise<ComposantSingleResult> => {
loading.value = true
try {
const normalizedPayload = normalizeRelationIds(buildConstructeurRequestPayload(composantData))
const result = await post('/composants', normalizedPayload)
if (result.success) {
const enriched = await withResolvedConstructeurs(result.data)
if (result.success && result.data) {
const enriched = await withResolvedConstructeurs(result.data as Composant)
composants.value.unshift(enriched)
total.value += 1
const displayName = result.data?.name
|| composantData?.definition?.name
|| composantData?.name
|| 'Composant'
const definition = (composantData as Record<string, unknown>)?.definition as Record<string, unknown> | undefined
const displayName =
(result.data as Composant)?.name ||
(definition?.name as string | undefined) ||
composantData?.name ||
'Composant'
showSuccess(`Composant "${displayName}" créé avec succès`)
return { success: true, data: enriched }
}
return result
return { success: false, error: result.error }
} catch (error) {
console.error('Erreur lors de la création du composant:', error)
return { success: false, error: error.message }
return { success: false, error: (error as Error).message }
} finally {
loading.value = false
}
}
const updateComposantData = async (id, composantData) => {
const updateComposantData = async (id: string, composantData: Partial<Composant>): Promise<ComposantSingleResult> => {
loading.value = true
try {
const normalizedPayload = normalizeRelationIds(buildConstructeurRequestPayload(composantData))
const result = await patch(`/composants/${id}`, normalizedPayload)
if (result.success) {
const updated = await withResolvedConstructeurs(result.data)
const index = composants.value.findIndex(comp => comp.id === id)
if (result.success && result.data) {
const updated = await withResolvedConstructeurs(result.data as Composant)
const index = composants.value.findIndex((comp) => comp.id === id)
if (index !== -1) {
composants.value[index] = updated
}
showSuccess(`Composant "${updated?.name || composantData.name || ''}" mis à jour avec succès`)
return { success: true, data: updated }
}
return result
return { success: false, error: result.error }
} catch (error) {
console.error('Erreur lors de la mise à jour du composant:', error)
return { success: false, error: error.message }
return { success: false, error: (error as Error).message }
} finally {
loading.value = false
}
}
const deleteComposant = async (id) => {
const deleteComposant = async (id: string): Promise<ComposantSingleResult> => {
loading.value = true
try {
const result = await del(`/composants/${id}`)
if (result.success) {
const deletedComposant = composants.value.find(comp => comp.id === id)
composants.value = composants.value.filter(comp => comp.id !== id)
const deletedComposant = composants.value.find((comp) => comp.id === id)
composants.value = composants.value.filter((comp) => comp.id !== id)
total.value = Math.max(0, total.value - 1)
showSuccess(`Composant "${deletedComposant?.name || 'inconnu'}" supprimé avec succès`)
return { success: true }
}
return result
return { success: false, error: result.error }
} catch (error) {
console.error('Erreur lors de la suppression du composant:', error)
return { success: false, error: error.message }
return { success: false, error: (error as Error).message }
} finally {
loading.value = false
}
@@ -208,6 +241,6 @@ export function useComposants () {
updateComposant: updateComposantData,
deleteComposant,
getComposants,
isLoading
isLoading,
}
}

View File

@@ -2,11 +2,24 @@ import { ref } from 'vue'
import { useApi } from './useApi'
import { useToast } from './useToast'
const constructeurs = ref([])
export interface Constructeur {
id: string
name: string
email?: string | null
phone?: string | null
}
interface ConstructeurResult {
success: boolean
data?: Constructeur | Constructeur[]
error?: string
}
const constructeurs = ref<Constructeur[]>([])
const loading = ref(false)
const uniqueConstructeurs = (items = []) => {
const map = new Map()
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)
@@ -15,7 +28,7 @@ const uniqueConstructeurs = (items = []) => {
return Array.from(map.values())
}
const normalizeIds = (ids = []) => {
const normalizeIds = (ids: unknown[] = []): string[] => {
if (!Array.isArray(ids)) {
return []
}
@@ -28,7 +41,7 @@ const normalizeIds = (ids = []) => {
)
}
const upsertConstructeurs = (items = []) => {
const upsertConstructeurs = (items: Constructeur[] = []) => {
if (!Array.isArray(items) || !items.length) {
return
}
@@ -36,32 +49,33 @@ const upsertConstructeurs = (items = []) => {
constructeurs.value = merged
}
const getIndexedConstructeur = (id) =>
const getIndexedConstructeur = (id: string): Constructeur | null =>
constructeurs.value.find((item) => item && item.id === id) || null
const extractCollection = (payload) => {
const extractCollection = (payload: unknown): Constructeur[] => {
if (Array.isArray(payload)) {
return payload
return payload as Constructeur[]
}
if (Array.isArray(payload?.member)) {
return payload.member
const p = payload as Record<string, unknown> | null
if (Array.isArray(p?.member)) {
return p.member as Constructeur[]
}
if (Array.isArray(payload?.['hydra:member'])) {
return payload['hydra:member']
if (Array.isArray(p?.['hydra:member'])) {
return p['hydra:member'] as Constructeur[]
}
if (Array.isArray(payload?.data)) {
return payload.data
if (Array.isArray(p?.data)) {
return p.data as Constructeur[]
}
return []
}
const pendingFetches = new Map()
const pendingFetches = new Map<string, Promise<Constructeur | null>>()
export function useConstructeurs () {
export function useConstructeurs() {
const { get, post, patch, delete: del } = useApi()
const { showSuccess, showError } = useToast()
const loadConstructeurs = async (search = '') => {
const loadConstructeurs = async (search = ''): Promise<ConstructeurResult> => {
loading.value = true
try {
const query = search ? `?search=${encodeURIComponent(search)}` : ''
@@ -70,47 +84,49 @@ export function useConstructeurs () {
const items = extractCollection(result.data)
constructeurs.value = uniqueConstructeurs(items)
}
return result
return result as ConstructeurResult
} catch (error) {
const err = error as Error
console.error('Erreur lors du chargement des fournisseurs:', error)
return { success: false, error: error.message }
return { success: false, error: err.message }
} finally {
loading.value = false
}
}
const searchConstructeurs = async (search = '') => {
const searchConstructeurs = async (search = ''): Promise<ConstructeurResult> => {
return loadConstructeurs(search)
}
const createConstructeur = async (data) => {
const createConstructeur = async (data: Partial<Constructeur>): Promise<ConstructeurResult> => {
loading.value = true
try {
const result = await post('/constructeurs', data)
if (result.success) {
upsertConstructeurs([result.data])
showSuccess(`Fournisseur "${result.data.name}" créé`)
upsertConstructeurs([result.data as Constructeur])
showSuccess(`Fournisseur "${(result.data as Constructeur).name}" créé`)
} else if (result.error) {
showError(result.error)
}
return result
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: error.message }
return { success: false, error: err.message }
} finally {
loading.value = false
}
}
const ensureConstructeurs = async (ids = []) => {
const ensureConstructeurs = async (ids: unknown[] = []): Promise<Constructeur[]> => {
const normalizedIds = normalizeIds(ids)
if (!normalizedIds.length) {
return []
}
const collected = []
const missing = []
const collected: Constructeur[] = []
const missing: string[] = []
normalizedIds.forEach((id) => {
const existing = getIndexedConstructeur(id)
if (existing) {
@@ -129,7 +145,7 @@ export function useConstructeurs () {
const task = get(`/constructeurs/${id}`)
.then((result) => {
if (result.success && result.data) {
return result.data
return result.data as Constructeur
}
return null
})
@@ -145,7 +161,7 @@ export function useConstructeurs () {
})
const fetched = await Promise.all(fetchTasks)
const validFetched = fetched.filter((item) => item && item.id)
const validFetched = fetched.filter((item): item is Constructeur => item !== null && item.id !== undefined)
if (validFetched.length) {
upsertConstructeurs(validFetched)
}
@@ -153,50 +169,52 @@ export function useConstructeurs () {
return normalizedIds
.map((id) => getIndexedConstructeur(id))
.filter((item) => Boolean(item))
.filter((item): item is Constructeur => item !== null)
}
const updateConstructeur = async (id, data) => {
const updateConstructeur = async (id: string, data: Partial<Constructeur>): Promise<ConstructeurResult> => {
loading.value = true
try {
const result = await patch(`/constructeurs/${id}`, data)
if (result.success) {
upsertConstructeurs([result.data])
showSuccess(`Fournisseur "${result.data.name}" mis à jour`)
upsertConstructeurs([result.data as Constructeur])
showSuccess(`Fournisseur "${(result.data as Constructeur).name}" mis à jour`)
} else if (result.error) {
showError(result.error)
}
return result
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: error.message }
return { success: false, error: err.message }
} finally {
loading.value = false
}
}
const deleteConstructeur = async (id) => {
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)
constructeurs.value = constructeurs.value.filter((item) => item.id !== id)
showSuccess('Fournisseur supprimé')
} else if (result.error) {
showError(result.error)
}
return result
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: error.message }
return { success: false, error: err.message }
} finally {
loading.value = false
}
}
const getConstructeurById = (id) => getIndexedConstructeur(id)
const getConstructeurById = (id: string) => getIndexedConstructeur(id)
return {
constructeurs,

View File

@@ -1,169 +0,0 @@
import { ref } from 'vue'
import { useApi } from './useApi'
import { useToast } from './useToast'
import { normalizeRelationIds } from '~/shared/apiRelations'
const documents = ref([])
const loading = ref(false)
const extractCollection = (payload) => {
if (Array.isArray(payload)) {
return payload
}
if (Array.isArray(payload?.member)) {
return payload.member
}
if (Array.isArray(payload?.['hydra:member'])) {
return payload['hydra:member']
}
if (Array.isArray(payload?.data)) {
return payload.data
}
return []
}
const fileToBase64 = file =>
new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = () => resolve(reader.result)
reader.onerror = () => reject(new Error(`Lecture du fichier ${file.name} impossible`))
reader.readAsDataURL(file)
})
export function useDocuments () {
const { get, post, delete: del } = useApi()
const { showError, showSuccess } = useToast()
const loadFromEndpoint = async (endpoint, { updateStore = false } = {}) => {
loading.value = true
try {
const result = await get(endpoint)
if (result.success) {
const data = extractCollection(result.data)
if (updateStore) {
documents.value = data
}
return { success: true, data }
}
if (result.error) {
showError(result.error)
}
return result
} catch (error) {
console.error(`Erreur lors du chargement des documents (${endpoint}):`, error)
showError('Impossible de charger les documents')
return { success: false, error: error.message }
} finally {
loading.value = false
}
}
const loadDocuments = async (options = {}) => {
return loadFromEndpoint('/documents', { updateStore: options.updateStore ?? true })
}
const loadDocumentsBySite = async (siteId, options = {}) => {
if (!siteId) { return { success: false, error: 'Aucun site sélectionné' } }
return loadFromEndpoint(`/documents/site/${siteId}`, { updateStore: options.updateStore ?? false })
}
const loadDocumentsByMachine = async (machineId, options = {}) => {
if (!machineId) { return { success: false, error: 'Aucune machine sélectionnée' } }
return loadFromEndpoint(`/documents/machine/${machineId}`, { updateStore: options.updateStore ?? false })
}
const loadDocumentsByComponent = async (componentId, options = {}) => {
if (!componentId) { return { success: false, error: 'Aucun composant sélectionné' } }
return loadFromEndpoint(`/documents/composant/${componentId}`, { updateStore: options.updateStore ?? false })
}
const loadDocumentsByProduct = async (productId, options = {}) => {
if (!productId) { return { success: false, error: 'Aucun produit sélectionné' } }
return loadFromEndpoint(`/documents/product/${productId}`, { updateStore: options.updateStore ?? false })
}
const loadDocumentsByPiece = async (pieceId, options = {}) => {
if (!pieceId) { return { success: false, error: 'Aucune pièce sélectionnée' } }
return loadFromEndpoint(`/documents/piece/${pieceId}`, { updateStore: options.updateStore ?? false })
}
const uploadDocuments = async ({ files = [], context = {} }, { updateStore = false } = {}) => {
if (!files.length) { return { success: false, error: 'Aucun fichier sélectionné' } }
loading.value = true
const created = []
try {
for (const file of files) {
const dataUrl = await fileToBase64(file)
const payload = normalizeRelationIds({
name: file.name,
filename: file.name,
mimeType: file.type || 'application/octet-stream',
size: file.size,
path: dataUrl,
...context
})
const result = await post('/documents', payload)
if (result.success) {
created.push(result.data)
showSuccess(`Document "${file.name}" ajouté`)
} else if (result.error) {
showError(`Erreur sur ${file.name} : ${result.error}`)
}
}
if (created.length) {
if (updateStore) {
documents.value = [...created, ...documents.value]
}
return { success: true, data: created }
}
return { success: false, error: 'Aucun document ajouté' }
} catch (error) {
console.error('Erreur lors de l\'upload des documents:', error)
showError("Échec de l'ajout des documents")
return { success: false, error: error.message }
} finally {
loading.value = false
}
}
const deleteDocument = async (id, { updateStore = false } = {}) => {
if (!id) { return { success: false, error: 'Identifiant manquant' } }
loading.value = true
try {
const result = await del(`/documents/${id}`)
if (result.success) {
if (updateStore) {
documents.value = documents.value.filter(doc => doc.id !== id)
}
showSuccess('Document supprimé')
}
return result
} catch (error) {
console.error('Erreur lors de la suppression du document:', error)
showError('Impossible de supprimer le document')
return { success: false, error: error.message }
} finally {
loading.value = false
}
}
return {
documents,
loading,
loadDocuments,
loadDocumentsBySite,
loadDocumentsByMachine,
loadDocumentsByComponent,
loadDocumentsByPiece,
loadDocumentsByProduct,
uploadDocuments,
deleteDocument
}
}

View File

@@ -0,0 +1,241 @@
import { ref } from 'vue'
import { useApi } from './useApi'
import { useToast } from './useToast'
import { normalizeRelationIds } from '~/shared/apiRelations'
export interface Document {
id: string
name: string
filename: string
mimeType: string
size: number
path: string
siteId?: string
machineId?: string
composantId?: string
productId?: string
pieceId?: string
}
export interface UploadContext {
siteId?: string
machineId?: string
composantId?: string
productId?: string
pieceId?: string
}
export interface DocumentResult {
success: boolean
data?: Document | Document[]
error?: string
}
const documents = ref<Document[]>([])
const loading = ref(false)
const extractCollection = (payload: unknown): Document[] => {
if (Array.isArray(payload)) {
return payload
}
const p = payload as Record<string, unknown> | null
if (Array.isArray(p?.member)) {
return p.member as Document[]
}
if (Array.isArray(p?.['hydra:member'])) {
return p['hydra:member'] as Document[]
}
if (Array.isArray(p?.data)) {
return p.data as Document[]
}
return []
}
const fileToBase64 = (file: File): Promise<string> =>
new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = () => resolve(reader.result as string)
reader.onerror = () => reject(new Error(`Lecture du fichier ${file.name} impossible`))
reader.readAsDataURL(file)
})
export function useDocuments() {
const { get, post, delete: del } = useApi()
const { showError, showSuccess } = useToast()
const loadFromEndpoint = async (
endpoint: string,
{ updateStore = false }: { updateStore?: boolean } = {},
): Promise<DocumentResult> => {
loading.value = true
try {
const result = await get(endpoint)
if (result.success) {
const data = extractCollection(result.data)
if (updateStore) {
documents.value = data
}
return { success: true, data }
}
if (result.error) {
showError(result.error)
}
return result as DocumentResult
} catch (error) {
const err = error as Error
console.error(`Erreur lors du chargement des documents (${endpoint}):`, error)
showError('Impossible de charger les documents')
return { success: false, error: err.message }
} finally {
loading.value = false
}
}
const loadDocuments = async (
options: { updateStore?: boolean } = {},
): Promise<DocumentResult> => {
return loadFromEndpoint('/documents', { updateStore: options.updateStore ?? true })
}
const loadDocumentsBySite = async (
siteId: string,
options: { updateStore?: boolean } = {},
): Promise<DocumentResult> => {
if (!siteId) {
return { success: false, error: 'Aucun site sélectionné' }
}
return loadFromEndpoint(`/documents/site/${siteId}`, { updateStore: options.updateStore ?? false })
}
const loadDocumentsByMachine = async (
machineId: string,
options: { updateStore?: boolean } = {},
): Promise<DocumentResult> => {
if (!machineId) {
return { success: false, error: 'Aucune machine sélectionnée' }
}
return loadFromEndpoint(`/documents/machine/${machineId}`, { updateStore: options.updateStore ?? false })
}
const loadDocumentsByComponent = async (
componentId: string,
options: { updateStore?: boolean } = {},
): Promise<DocumentResult> => {
if (!componentId) {
return { success: false, error: 'Aucun composant sélectionné' }
}
return loadFromEndpoint(`/documents/composant/${componentId}`, { updateStore: options.updateStore ?? false })
}
const loadDocumentsByProduct = async (
productId: string,
options: { updateStore?: boolean } = {},
): Promise<DocumentResult> => {
if (!productId) {
return { success: false, error: 'Aucun produit sélectionné' }
}
return loadFromEndpoint(`/documents/product/${productId}`, { updateStore: options.updateStore ?? false })
}
const loadDocumentsByPiece = async (
pieceId: string,
options: { updateStore?: boolean } = {},
): Promise<DocumentResult> => {
if (!pieceId) {
return { success: false, error: 'Aucune pièce sélectionnée' }
}
return loadFromEndpoint(`/documents/piece/${pieceId}`, { updateStore: options.updateStore ?? false })
}
const uploadDocuments = async (
{ files, context = {} }: { files: File[]; context?: UploadContext },
{ updateStore = false }: { updateStore?: boolean } = {},
): Promise<DocumentResult> => {
if (!files.length) {
return { success: false, error: 'Aucun fichier sélectionné' }
}
loading.value = true
const created: Document[] = []
try {
for (const file of files) {
const dataUrl = await fileToBase64(file)
const payload = normalizeRelationIds({
name: file.name,
filename: file.name,
mimeType: file.type || 'application/octet-stream',
size: file.size,
path: dataUrl,
...context,
})
const result = await post('/documents', payload)
if (result.success) {
created.push(result.data as Document)
showSuccess(`Document "${file.name}" ajouté`)
} else if (result.error) {
showError(`Erreur sur ${file.name} : ${result.error}`)
}
}
if (created.length) {
if (updateStore) {
documents.value = [...created, ...documents.value]
}
return { success: true, data: created }
}
return { success: false, error: 'Aucun document ajouté' }
} catch (error) {
const err = error as Error
console.error("Erreur lors de l'upload des documents:", error)
showError("Échec de l'ajout des documents")
return { success: false, error: err.message }
} finally {
loading.value = false
}
}
const deleteDocument = async (
id: string | number,
{ updateStore = false }: { updateStore?: boolean } = {},
): Promise<DocumentResult> => {
if (!id) {
return { success: false, error: 'Identifiant manquant' }
}
loading.value = true
try {
const result = await del(`/documents/${id}`)
if (result.success) {
if (updateStore) {
documents.value = documents.value.filter((doc) => doc.id !== id)
}
showSuccess('Document supprimé')
}
return result as DocumentResult
} catch (error) {
const err = error as Error
console.error('Erreur lors de la suppression du document:', error)
showError('Impossible de supprimer le document')
return { success: false, error: err.message }
} finally {
loading.value = false
}
}
return {
documents,
loading,
loadDocuments,
loadDocumentsBySite,
loadDocumentsByMachine,
loadDocumentsByComponent,
loadDocumentsByPiece,
loadDocumentsByProduct,
uploadDocuments,
deleteDocument,
}
}

View File

@@ -1,14 +1,34 @@
import { ref } from 'vue'
import { useToast } from './useToast'
import { listModelTypes, createModelType, updateModelType, deleteModelType } from '~/services/modelTypes'
import { listModelTypes, createModelType, updateModelType, deleteModelType, type ModelType } from '~/services/modelTypes'
import type { PieceModelStructure } from '~/shared/types/inventory'
const pieceTypes = ref([])
export interface PieceType extends ModelType {
structure: PieceModelStructure | null
description?: string | null
}
interface PieceTypePayload {
name: string
code?: string
description?: string | null
notes?: string | null
structure?: PieceModelStructure | null
}
interface PieceTypeResult {
success: boolean
data?: PieceType | PieceType[]
error?: string
}
const pieceTypes = ref<PieceType[]>([])
const loadingPieceTypes = ref(false)
export function usePieceTypes () {
export function usePieceTypes() {
const { showSuccess, showError } = useToast()
const generateCodeFromName = (name) => {
const generateCodeFromName = (name: string): string => {
return (name || '')
.normalize('NFD')
.replace(/[\u0300-\u036F]/g, '')
@@ -18,24 +38,26 @@ export function usePieceTypes () {
.replace(/-+/g, '-') || 'type'
}
const loadPieceTypes = async () => {
const loadPieceTypes = async (): Promise<PieceTypeResult> => {
loadingPieceTypes.value = true
try {
const data = await listModelTypes({
category: 'PIECE',
sort: 'name',
dir: 'asc',
limit: 200
limit: 200,
})
pieceTypes.value = data.items.map(item => ({
pieceTypes.value = data.items.map((item) => ({
...item,
description: item.description ?? item.notes ?? null
structure: item.structure as PieceModelStructure | null,
description: item.description ?? item.notes ?? null,
}))
return { success: true, data: pieceTypes.value }
} catch (error) {
const message = error?.message || 'Erreur inconnue'
const err = error as Error & { message?: string }
const message = err?.message || 'Erreur inconnue'
showError(`Impossible de charger les types de pièce: ${message}`)
return { success: false, error: message }
} finally {
@@ -43,21 +65,22 @@ export function usePieceTypes () {
}
}
const createPieceType = async (payload) => {
const createPieceType = async (payload: PieceTypePayload): Promise<PieceTypeResult> => {
loadingPieceTypes.value = true
try {
const data = await createModelType({
name: payload.name,
code: payload.code || generateCodeFromName(payload.name),
category: 'PIECE',
notes: payload.description ?? payload.notes,
description: payload.description ?? null,
structure: payload.structure
notes: payload.description ?? payload.notes ?? undefined,
description: payload.description ?? undefined,
structure: payload.structure ?? undefined,
})
const normalized = {
const normalized: PieceType = {
...data,
description: data.description ?? data.notes ?? null
structure: data.structure as PieceModelStructure | null,
description: data.description ?? data.notes ?? null,
}
pieceTypes.value.push(normalized)
@@ -65,7 +88,8 @@ export function usePieceTypes () {
return { success: true, data: normalized }
} catch (error) {
const message = error?.data?.message || error?.message || 'Erreur inconnue'
const err = error as Error & { data?: { message?: string }; message?: string }
const message = err?.data?.message || err?.message || 'Erreur inconnue'
showError(`Erreur lors de la création du type de pièce: ${message}`)
return { success: false, error: message }
} finally {
@@ -73,34 +97,33 @@ export function usePieceTypes () {
}
}
const updatePieceType = async (id, payload) => {
const updatePieceType = async (id: string, payload: PieceTypePayload): Promise<PieceTypeResult> => {
loadingPieceTypes.value = true
try {
const data = await updateModelType(id, {
name: payload.name,
description: payload.description,
notes: payload.notes,
description: payload.description ?? undefined,
notes: payload.notes ?? undefined,
code: payload.code,
structure: payload.structure
structure: payload.structure ?? undefined,
})
const normalized = {
const normalized: PieceType = {
...data,
description: data.description ?? data.notes ?? null
structure: data.structure as PieceModelStructure | null,
description: data.description ?? data.notes ?? null,
}
const index = pieceTypes.value.findIndex(type => type.id === id)
const index = pieceTypes.value.findIndex((type) => type.id === id)
if (index !== -1) {
pieceTypes.value[index] = normalized
}
showSuccess(`Type de pièce "${data.name}" mis à jour`)
return {
success: true,
data: normalized
}
return { success: true, data: normalized }
} catch (error) {
const message = error?.data?.message || error?.message || 'Erreur inconnue'
const err = error as Error & { data?: { message?: string }; message?: string }
const message = err?.data?.message || err?.message || 'Erreur inconnue'
showError(`Erreur lors de la mise à jour du type de pièce: ${message}`)
return { success: false, error: message }
} finally {
@@ -108,15 +131,16 @@ export function usePieceTypes () {
}
}
const deletePieceType = async (id) => {
const deletePieceType = async (id: string): Promise<PieceTypeResult> => {
loadingPieceTypes.value = true
try {
await deleteModelType(id)
pieceTypes.value = pieceTypes.value.filter(type => type.id !== id)
pieceTypes.value = pieceTypes.value.filter((type) => type.id !== id)
showSuccess('Type de pièce supprimé')
return { success: true }
} catch (error) {
const message = error?.data?.message || error?.message || 'Erreur inconnue'
const err = error as Error & { data?: { message?: string }; message?: string }
const message = err?.data?.message || err?.message || 'Erreur inconnue'
showError(`Erreur lors de la suppression du type de pièce: ${message}`)
return { success: false, error: message }
} finally {
@@ -135,6 +159,6 @@ export function usePieceTypes () {
updatePieceType,
deletePieceType,
getPieceTypes,
isPieceTypeLoading
isPieceTypeLoading,
}
}

View File

@@ -2,45 +2,84 @@ import { ref } from 'vue'
import { useToast } from './useToast'
import { useApi } from './useApi'
import { buildConstructeurRequestPayload, uniqueConstructeurIds } from '~/shared/constructeurUtils'
import { useConstructeurs } from './useConstructeurs'
import { useConstructeurs, type Constructeur } from './useConstructeurs'
import { extractRelationId, normalizeRelationIds } from '~/shared/apiRelations'
const pieces = ref([])
export interface Piece {
id: string
name: string
reference?: 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'
}
const pieces = ref<Piece[]>([])
const total = ref(0)
const loading = ref(false)
const extractCollection = (payload) => {
const extractCollection = (payload: unknown): Piece[] => {
if (Array.isArray(payload)) {
return payload
return payload as Piece[]
}
if (Array.isArray(payload?.member)) {
return payload.member
const p = payload as Record<string, unknown> | null
if (Array.isArray(p?.member)) {
return p.member as Piece[]
}
if (Array.isArray(payload?.['hydra:member'])) {
return payload['hydra:member']
if (Array.isArray(p?.['hydra:member'])) {
return p['hydra:member'] as Piece[]
}
if (Array.isArray(payload?.data)) {
return payload.data
if (Array.isArray(p?.data)) {
return p.data as Piece[]
}
return []
}
const extractTotal = (payload, fallbackLength) => {
if (typeof payload?.totalItems === 'number') {
return payload.totalItems
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 payload?.['hydra:totalItems'] === 'number') {
return payload['hydra:totalItems']
if (typeof p?.['hydra:totalItems'] === 'number') {
return p['hydra:totalItems']
}
return fallbackLength
}
export function usePieces () {
const { showSuccess, showError, showInfo } = useToast()
export function usePieces() {
const { showSuccess } = useToast()
const { get, post, patch, delete: del } = useApi()
const { ensureConstructeurs } = useConstructeurs()
const withResolvedConstructeurs = async (piece) => {
const withResolvedConstructeurs = async (piece: Piece): Promise<Piece> => {
if (!piece || typeof piece !== 'object') {
return piece
}
@@ -68,12 +107,11 @@ export function usePieces () {
const ids = uniqueConstructeurIds(
piece.constructeurIds,
piece.constructeurs,
piece.constructeur,
)
const hasResolvedConstructeurs =
Array.isArray(piece.constructeurs)
&& piece.constructeurs.length > 0
&& piece.constructeurs.every((item) => item && typeof item === 'object')
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)
@@ -85,16 +123,7 @@ export function usePieces () {
return piece
}
/**
* Load pieces with pagination and search support
* @param {Object} options - Query options
* @param {string} [options.search] - Search term for name/reference
* @param {number} [options.page=1] - Current page (1-based)
* @param {number} [options.itemsPerPage=30] - Items per page
* @param {string} [options.orderBy='name'] - Field to order by
* @param {string} [options.orderDir='asc'] - Order direction (asc/desc)
*/
const loadPieces = async (options = {}) => {
const loadPieces = async (options: LoadPiecesOptions = {}): Promise<PieceListResult> => {
loading.value = true
try {
const {
@@ -102,7 +131,7 @@ export function usePieces () {
page = 1,
itemsPerPage = 30,
orderBy = 'name',
orderDir = 'asc'
orderDir = 'asc',
} = options
const params = new URLSearchParams()
@@ -110,11 +139,9 @@ export function usePieces () {
params.set('page', String(page))
if (search && search.trim()) {
// API Platform uses property filters
params.set('name', search.trim())
}
// API Platform OrderFilter syntax: order[field]=direction
params.set(`order[${orderBy}]`, orderDir)
const result = await get(`/pieces?${params.toString()}`)
@@ -129,79 +156,84 @@ export function usePieces () {
items: enrichedItems,
total: total.value,
page,
itemsPerPage
}
itemsPerPage,
},
}
}
return result
return result as PieceListResult
} catch (error) {
console.error('Erreur lors du chargement des pièces:', error)
return { success: false, error: error.message }
return { success: false, error: (error as Error).message }
} finally {
loading.value = false
}
}
const createPiece = async (pieceData) => {
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) {
const enriched = await withResolvedConstructeurs(result.data)
if (result.success && result.data) {
const enriched = await withResolvedConstructeurs(result.data as Piece)
pieces.value.unshift(enriched)
total.value += 1
const displayName = result.data?.name
|| pieceData?.definition?.name
|| pieceData?.name
|| 'Pièce'
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 result
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.message }
return { success: false, error: (error as Error).message }
} finally {
loading.value = false
}
}
const updatePieceData = async (id, pieceData) => {
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) {
const updated = await withResolvedConstructeurs(result.data)
const index = pieces.value.findIndex(piece => piece.id === id)
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 result
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.message }
return { success: false, error: (error as Error).message }
} finally {
loading.value = false
}
}
const deletePiece = async (id) => {
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)
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 result
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.message }
return { success: false, error: (error as Error).message }
} finally {
loading.value = false
}
@@ -219,6 +251,6 @@ export function usePieces () {
updatePiece: updatePieceData,
deletePiece,
getPieces,
isLoading
isLoading,
}
}

View File

@@ -1,14 +1,34 @@
import { ref } from 'vue'
import { useToast } from './useToast'
import { listModelTypes, createModelType, updateModelType, deleteModelType } from '~/services/modelTypes'
import { listModelTypes, createModelType, updateModelType, deleteModelType, type ModelType } from '~/services/modelTypes'
import type { ProductModelStructure } from '~/shared/types/inventory'
const productTypes = ref([])
export interface ProductType extends ModelType {
structure: ProductModelStructure | null
description?: string | null
}
interface ProductTypePayload {
name: string
code?: string
description?: string | null
notes?: string | null
structure?: ProductModelStructure | null
}
interface ProductTypeResult {
success: boolean
data?: ProductType | ProductType[]
error?: string
}
const productTypes = ref<ProductType[]>([])
const loadingProductTypes = ref(false)
export function useProductTypes () {
export function useProductTypes() {
const { showSuccess, showError } = useToast()
const generateCodeFromName = (name) => {
const generateCodeFromName = (name: string): string => {
return (name || '')
.normalize('NFD')
.replace(/[\u0300-\u036F]/g, '')
@@ -18,7 +38,7 @@ export function useProductTypes () {
.replace(/-+/g, '-') || 'type'
}
const loadProductTypes = async () => {
const loadProductTypes = async (): Promise<ProductTypeResult> => {
loadingProductTypes.value = true
try {
const data = await listModelTypes({
@@ -28,14 +48,16 @@ export function useProductTypes () {
limit: 200,
})
productTypes.value = data.items.map(item => ({
productTypes.value = data.items.map((item) => ({
...item,
structure: item.structure as ProductModelStructure | null,
description: item.description ?? item.notes ?? null,
}))
return { success: true, data: productTypes.value }
} catch (error) {
const message = error?.message || 'Erreur inconnue'
const err = error as Error & { message?: string }
const message = err?.message || 'Erreur inconnue'
showError(`Impossible de charger les types de produit: ${message}`)
return { success: false, error: message }
} finally {
@@ -43,20 +65,21 @@ export function useProductTypes () {
}
}
const createProductType = async (payload) => {
const createProductType = async (payload: ProductTypePayload): Promise<ProductTypeResult> => {
loadingProductTypes.value = true
try {
const data = await createModelType({
name: payload.name,
code: payload.code || generateCodeFromName(payload.name),
category: 'PRODUCT',
notes: payload.description ?? payload.notes,
description: payload.description ?? null,
structure: payload.structure,
notes: payload.description ?? payload.notes ?? undefined,
description: payload.description ?? undefined,
structure: payload.structure ?? undefined,
})
const normalized = {
const normalized: ProductType = {
...data,
structure: data.structure as ProductModelStructure | null,
description: data.description ?? data.notes ?? null,
}
@@ -65,7 +88,8 @@ export function useProductTypes () {
return { success: true, data: normalized }
} catch (error) {
const message = error?.data?.message || error?.message || 'Erreur inconnue'
const err = error as Error & { data?: { message?: string }; message?: string }
const message = err?.data?.message || err?.message || 'Erreur inconnue'
showError(`Erreur lors de la création du type de produit: ${message}`)
return { success: false, error: message }
} finally {
@@ -73,23 +97,24 @@ export function useProductTypes () {
}
}
const updateProductType = async (id, payload) => {
const updateProductType = async (id: string, payload: ProductTypePayload): Promise<ProductTypeResult> => {
loadingProductTypes.value = true
try {
const data = await updateModelType(id, {
name: payload.name,
description: payload.description,
notes: payload.notes,
description: payload.description ?? undefined,
notes: payload.notes ?? undefined,
code: payload.code,
structure: payload.structure,
structure: payload.structure ?? undefined,
})
const normalized = {
const normalized: ProductType = {
...data,
structure: data.structure as ProductModelStructure | null,
description: data.description ?? data.notes ?? null,
}
const index = productTypes.value.findIndex(type => type.id === id)
const index = productTypes.value.findIndex((type) => type.id === id)
if (index !== -1) {
productTypes.value[index] = normalized
}
@@ -97,7 +122,8 @@ export function useProductTypes () {
return { success: true, data: normalized }
} catch (error) {
const message = error?.data?.message || error?.message || 'Erreur inconnue'
const err = error as Error & { data?: { message?: string }; message?: string }
const message = err?.data?.message || err?.message || 'Erreur inconnue'
showError(`Erreur lors de la mise à jour du type de produit: ${message}`)
return { success: false, error: message }
} finally {
@@ -105,15 +131,16 @@ export function useProductTypes () {
}
}
const deleteProductType = async (id) => {
const deleteProductType = async (id: string): Promise<ProductTypeResult> => {
loadingProductTypes.value = true
try {
await deleteModelType(id)
productTypes.value = productTypes.value.filter(type => type.id !== id)
productTypes.value = productTypes.value.filter((type) => type.id !== id)
showSuccess('Type de produit supprimé')
return { success: true }
} catch (error) {
const message = error?.data?.message || error?.message || 'Erreur inconnue'
const err = error as Error & { data?: { message?: string }; message?: string }
const message = err?.data?.message || err?.message || 'Erreur inconnue'
showError(`Erreur lors de la suppression du type de produit: ${message}`)
return { success: false, error: message }
} finally {

View File

@@ -2,16 +2,52 @@ import { ref } from 'vue'
import { useToast } from './useToast'
import { useApi } from './useApi'
import { buildConstructeurRequestPayload, uniqueConstructeurIds } from '~/shared/constructeurUtils'
import { useConstructeurs } from './useConstructeurs'
import { useConstructeurs, type Constructeur } from './useConstructeurs'
import { extractRelationId, normalizeRelationIds } from '~/shared/apiRelations'
const products = ref([])
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'
force?: boolean
}
const products = ref<Product[]>([])
const total = ref(0)
const loading = ref(false)
const loaded = ref(false)
const error = ref(null)
const error = ref<string | null>(null)
const replaceInCache = (item) => {
const replaceInCache = (item: Product): boolean => {
if (!item?.id) {
return false
}
@@ -26,38 +62,40 @@ const replaceInCache = (item) => {
return false
}
const extractCollection = (payload) => {
const extractCollection = (payload: unknown): Product[] => {
if (Array.isArray(payload)) {
return payload
return payload as Product[]
}
if (Array.isArray(payload?.member)) {
return payload.member
const p = payload as Record<string, unknown> | null
if (Array.isArray(p?.member)) {
return p.member as Product[]
}
if (Array.isArray(payload?.['hydra:member'])) {
return payload['hydra:member']
if (Array.isArray(p?.['hydra:member'])) {
return p['hydra:member'] as Product[]
}
if (Array.isArray(payload?.data)) {
return payload.data
if (Array.isArray(p?.data)) {
return p.data as Product[]
}
return []
}
const extractTotal = (payload, fallbackLength) => {
if (typeof payload?.totalItems === 'number') {
return payload.totalItems
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 payload?.['hydra:totalItems'] === 'number') {
return payload['hydra:totalItems']
if (typeof p?.['hydra:totalItems'] === 'number') {
return p['hydra:totalItems']
}
return fallbackLength
}
export function useProducts () {
export function useProducts() {
const { showError } = useToast()
const { get, post, patch, delete: del } = useApi()
const { ensureConstructeurs } = useConstructeurs()
const withResolvedConstructeurs = async (product) => {
const withResolvedConstructeurs = async (product: Product): Promise<Product> => {
if (!product || typeof product !== 'object') {
return product
}
@@ -70,12 +108,11 @@ export function useProducts () {
const ids = uniqueConstructeurIds(
product.constructeurIds,
product.constructeurs,
product.constructeur,
)
const hasResolvedConstructeurs =
Array.isArray(product.constructeurs)
&& product.constructeurs.length > 0
&& product.constructeurs.every((item) => item && typeof item === 'object')
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)
@@ -87,24 +124,13 @@ export function useProducts () {
return product
}
/**
* Load products with pagination and search support
* @param {Object} options - Query options
* @param {string} [options.search] - Search term for name/reference
* @param {number} [options.page=1] - Current page (1-based)
* @param {number} [options.itemsPerPage=30] - Items per page
* @param {string} [options.orderBy='name'] - Field to order by
* @param {string} [options.orderDir='asc'] - Order direction (asc/desc)
* @param {boolean} [options.force=false] - Force reload even if already loaded
*/
const loadProducts = async (options = {}) => {
const loadProducts = async (options: LoadProductsOptions = {}): Promise<ProductListResult> => {
const {
search = '',
page = 1,
itemsPerPage = 30,
orderBy = 'name',
orderDir = 'asc',
force = false
} = options
if (loading.value) {
@@ -140,17 +166,17 @@ export function useProducts () {
items: enrichedItems,
total: total.value,
page,
itemsPerPage
}
itemsPerPage,
},
}
} else if (result.error) {
error.value = result.error
showError(`Impossible de charger les produits: ${result.error}`)
}
return result
return result as ProductListResult
} catch (err) {
console.error('Erreur lors du chargement des produits:', err)
const message = err?.message ?? 'Erreur inconnue'
const message = (err as Error)?.message ?? 'Erreur inconnue'
error.value = message
showError(`Impossible de charger les produits: ${message}`)
return { success: false, error: message }
@@ -159,26 +185,27 @@ export function useProducts () {
}
}
const createProduct = async (payload) => {
const createProduct = async (payload: Partial<Product>): Promise<ProductSingleResult> => {
const normalizedPayload = normalizeRelationIds(buildConstructeurRequestPayload(payload))
loading.value = true
error.value = null
try {
const result = await post('/products', normalizedPayload)
if (result.success && result.data) {
const enriched = await withResolvedConstructeurs(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 result
return { success: false, error: result.error }
} catch (err) {
console.error('Erreur lors de la création du produit:', err)
const message = err?.message ?? 'Erreur inconnue'
const message = (err as Error)?.message ?? 'Erreur inconnue'
error.value = message
showError(message)
return { success: false, error: message }
@@ -187,23 +214,24 @@ export function useProducts () {
}
}
const updateProduct = async (id, payload) => {
const updateProduct = async (id: string, payload: Partial<Product>): Promise<ProductSingleResult> => {
const normalizedPayload = normalizeRelationIds(buildConstructeurRequestPayload(payload))
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)
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 result
return { success: false, error: result.error }
} catch (err) {
console.error('Erreur lors de la mise à jour du produit:', err)
const message = err?.message ?? 'Erreur inconnue'
const message = (err as Error)?.message ?? 'Erreur inconnue'
error.value = message
showError(message)
return { success: false, error: message }
@@ -212,23 +240,23 @@ export function useProducts () {
}
}
const deleteProduct = async (id) => {
const deleteProduct = async (id: string): Promise<ProductSingleResult> => {
loading.value = true
error.value = null
try {
const result = await del(`/products/${id}`)
if (result.success) {
const removed = products.value.find((product) => product.id === id)
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 result
return { success: false, error: result.error }
} catch (err) {
console.error('Erreur lors de la suppression du produit:', err)
const message = err?.message ?? 'Erreur inconnue'
const message = (err as Error)?.message ?? 'Erreur inconnue'
error.value = message
showError(message)
return { success: false, error: message }
@@ -237,7 +265,7 @@ export function useProducts () {
}
}
const getProduct = async (id, options = {}) => {
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)
@@ -249,14 +277,14 @@ export function useProducts () {
try {
const result = await get(`/products/${id}`)
if (result.success && result.data) {
const enriched = await withResolvedConstructeurs(result.data)
const enriched = await withResolvedConstructeurs(result.data as Product)
replaceInCache(enriched)
return { success: true, data: enriched }
}
return result
return { success: false, error: result.error }
} catch (err) {
console.error('Erreur lors du chargement du produit:', err)
const message = err?.message ?? 'Erreur inconnue'
const message = (err as Error)?.message ?? 'Erreur inconnue'
return { success: false, error: message }
}
}

View File

@@ -166,8 +166,9 @@ export function useSiteManagement() {
)
uploadingDocuments.value = false
if (uploadResult.success) {
uploadedDocuments = uploadResult.data || []
if (uploadResult.success && uploadResult.data) {
const data = uploadResult.data
uploadedDocuments = (Array.isArray(data) ? data : [data]) as SiteDocument[]
selectedFiles.value = []
}
}

View File

@@ -1,111 +0,0 @@
import { ref } from 'vue'
import { useToast } from './useToast'
import { useApi } from './useApi'
const sites = ref([])
const loading = ref(false)
export function useSites () {
const { showSuccess, showInfo } = useToast()
const { get, post, patch, delete: del } = useApi()
const loadSites = async () => {
loading.value = true
try {
const result = await get('/sites')
console.log('sites api result', result)
if (result.success) {
const collection = Array.isArray(result.data)
? result.data
: Array.isArray(result.data?.member)
? result.data.member
: Array.isArray(result.data?.['hydra:member'])
? result.data['hydra:member']
: Array.isArray(result.data?.data)
? result.data.data
: []
sites.value = collection
showInfo(`Chargement de ${collection.length} site(s) réussi`)
}
} catch (error) {
console.error('Erreur lors du chargement des sites:', error)
} finally {
loading.value = false
}
}
const createSite = async (siteData) => {
loading.value = true
try {
const result = await post('/sites', siteData)
if (result.success) {
sites.value.push(result.data)
showSuccess(`Site "${siteData.name}" créé avec succès`)
}
return result
} catch (error) {
console.error('Erreur lors de la création du site:', error)
return { success: false, error: error.message }
} finally {
loading.value = false
}
}
const updateSite = async (id, siteData) => {
loading.value = true
try {
const result = await patch(`/sites/${id}`, siteData)
if (result.success) {
const index = sites.value.findIndex(site => site.id === id)
if (index !== -1) {
sites.value[index] = result.data
}
showSuccess(`Site "${siteData.name}" mis à jour avec succès`)
}
return result
} catch (error) {
console.error('Erreur lors de la mise à jour du site:', error)
return { success: false, error: error.message }
} finally {
loading.value = false
}
}
const deleteSite = async (id) => {
loading.value = true
try {
const result = await del(`/sites/${id}`)
if (result.success) {
const deletedSite = sites.value.find(site => site.id === id)
sites.value = sites.value.filter(site => site.id !== id)
showSuccess(`Site "${deletedSite?.name || 'inconnu'}" supprimé avec succès`)
}
return result
} catch (error) {
console.error('Erreur lors de la suppression du site:', error)
return { success: false, error: error.message }
} finally {
loading.value = false
}
}
const getSiteById = (id) => {
return sites.value.find(site => site.id === id)
}
const getSites = () => sites.value
const isLoading = () => loading.value
return {
sites,
loading,
loadSites,
createSite,
updateSite,
deleteSite,
getSiteById,
getSites,
isLoading
}
}

139
app/composables/useSites.ts Normal file
View File

@@ -0,0 +1,139 @@
import { ref } from 'vue'
import { useToast } from './useToast'
import { useApi } from './useApi'
export interface Site {
id: string
name?: string
contactName?: string
contactPhone?: string
contactAddress?: string
contactPostalCode?: string
contactCity?: string
machines?: unknown[]
documents?: unknown[]
[key: string]: unknown
}
interface SiteResult {
success: boolean
data?: Site
error?: string
}
const sites = ref<Site[]>([])
const loading = ref(false)
const extractCollection = (payload: unknown): Site[] => {
if (Array.isArray(payload)) {
return payload as Site[]
}
const p = payload as Record<string, unknown> | null
if (Array.isArray(p?.member)) {
return p.member as Site[]
}
if (Array.isArray(p?.['hydra:member'])) {
return p['hydra:member'] as Site[]
}
if (Array.isArray(p?.data)) {
return p.data as Site[]
}
return []
}
export function useSites() {
const { showSuccess, showInfo } = useToast()
const { get, post, patch, delete: del } = useApi()
const loadSites = async (): Promise<void> => {
loading.value = true
try {
const result = await get('/sites')
console.log('sites api result', result)
if (result.success) {
const collection = extractCollection(result.data)
sites.value = collection
showInfo(`Chargement de ${collection.length} site(s) réussi`)
}
} catch (error) {
console.error('Erreur lors du chargement des sites:', error)
} finally {
loading.value = false
}
}
const createSite = async (siteData: Partial<Site>): Promise<SiteResult> => {
loading.value = true
try {
const result = await post('/sites', siteData)
if (result.success && result.data) {
sites.value.push(result.data as Site)
showSuccess(`Site "${siteData.name}" créé avec succès`)
}
return result as SiteResult
} catch (error) {
console.error('Erreur lors de la création du site:', error)
return { success: false, error: (error as Error).message }
} finally {
loading.value = false
}
}
const updateSite = async (id: string, siteData: Partial<Site>): Promise<SiteResult> => {
loading.value = true
try {
const result = await patch(`/sites/${id}`, siteData)
if (result.success && result.data) {
const index = sites.value.findIndex((site) => site.id === id)
if (index !== -1) {
sites.value[index] = result.data as Site
}
showSuccess(`Site "${siteData.name}" mis à jour avec succès`)
}
return result as SiteResult
} catch (error) {
console.error('Erreur lors de la mise à jour du site:', error)
return { success: false, error: (error as Error).message }
} finally {
loading.value = false
}
}
const deleteSite = async (id: string): Promise<SiteResult> => {
loading.value = true
try {
const result = await del(`/sites/${id}`)
if (result.success) {
const deletedSite = sites.value.find((site) => site.id === id)
sites.value = sites.value.filter((site) => site.id !== id)
showSuccess(`Site "${deletedSite?.name || 'inconnu'}" supprimé avec succès`)
}
return result as SiteResult
} catch (error) {
console.error('Erreur lors de la suppression du site:', error)
return { success: false, error: (error as Error).message }
} finally {
loading.value = false
}
}
const getSiteById = (id: string): Site | undefined => {
return sites.value.find((site) => site.id === id)
}
const getSites = () => sites.value
const isLoading = () => loading.value
return {
sites,
loading,
loadSites,
createSite,
updateSite,
deleteSite,
getSiteById,
getSites,
isLoading,
}
}