From a6664ce9a2d5b84b8cb5fc994850c98649ea39be Mon Sep 17 00:00:00 2001 From: Matthieu Date: Mon, 9 Feb 2026 11:13:39 +0100 Subject: [PATCH] refactor(composables): merge 3 type composables into generic (F2.3) Create useEntityTypes.ts with CRUD + singleton state by category. Rewrite useComponentTypes, usePieceTypes, useProductTypes as thin wrappers that rename fields for backward compatibility. Co-Authored-By: Claude Opus 4.6 --- app/composables/useComponentTypes.ts | 173 +++------------------------ app/composables/useEntityTypes.ts | 171 ++++++++++++++++++++++++++ app/composables/usePieceTypes.ts | 173 +++------------------------ app/composables/useProductTypes.ts | 166 +++---------------------- 4 files changed, 226 insertions(+), 457 deletions(-) create mode 100644 app/composables/useEntityTypes.ts diff --git a/app/composables/useComponentTypes.ts b/app/composables/useComponentTypes.ts index cd0f9bd..147acdc 100644 --- a/app/composables/useComponentTypes.ts +++ b/app/composables/useComponentTypes.ts @@ -1,164 +1,29 @@ -import { ref } from 'vue' -import { useToast } from './useToast' -import { listModelTypes, createModelType, updateModelType, deleteModelType, type ModelType } from '~/services/modelTypes' +/** + * Backward-compatible wrapper around useEntityTypes. + * Preserves the original API surface (renamed fields) so consumers need no changes. + */ +import { useEntityTypes, type EntityType } from './useEntityTypes' import type { ComponentModelStructure } from '~/shared/types/inventory' +import type { Ref } from 'vue' -export interface ComponentType extends ModelType { +export interface ComponentType extends EntityType { 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([]) -const loadingComponentTypes = ref(false) - export function useComponentTypes() { - const { showSuccess, showError } = useToast() - - const generateCodeFromName = (name: string): string => { - return (name || '') - .normalize('NFD') - .replace(/[\u0300-\u036F]/g, '') - .toLowerCase() - .replace(/[^a-z0-9]+/g, '-') - .replace(/^-+|-+$/g, '') - .replace(/-+/g, '-') || 'type' - } - - const loadComponentTypes = async (): Promise => { - loadingComponentTypes.value = true - try { - const data = await listModelTypes({ - category: 'COMPONENT', - sort: 'name', - dir: 'asc', - limit: 200, - }) - - componentTypes.value = data.items.map((item) => ({ - ...item, - structure: item.structure as ComponentModelStructure | null, - description: item.description ?? item.notes ?? null, - })) - - return { success: true, data: componentTypes.value } - } catch (error) { - 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 { - loadingComponentTypes.value = false - } - } - - const createComponentType = async (payload: ComponentTypePayload): Promise => { - loadingComponentTypes.value = true - try { - const data = await createModelType({ - name: payload.name, - code: payload.code || generateCodeFromName(payload.name), - category: 'COMPONENT', - notes: payload.description ?? payload.notes ?? undefined, - description: payload.description ?? undefined, - structure: payload.structure ?? undefined, - }) - - const normalized: ComponentType = { - ...data, - structure: data.structure as ComponentModelStructure | null, - description: data.description ?? data.notes ?? null, - } - - componentTypes.value.push(normalized) - showSuccess(`Type de composant "${data.name}" créé`) - - return { success: true, data: normalized } - } catch (error) { - 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 { - loadingComponentTypes.value = false - } - } - - const updateComponentType = async (id: string, payload: ComponentTypePayload): Promise => { - loadingComponentTypes.value = true - try { - const data = await updateModelType(id, { - name: payload.name, - description: payload.description ?? undefined, - notes: payload.notes ?? undefined, - code: payload.code, - structure: payload.structure ?? undefined, - }) - - const normalized: ComponentType = { - ...data, - structure: data.structure as ComponentModelStructure | null, - description: data.description ?? data.notes ?? null, - } - - 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 } - } catch (error) { - 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 { - loadingComponentTypes.value = false - } - } - - const deleteComponentType = async (id: string): Promise => { - loadingComponentTypes.value = true - try { - await deleteModelType(id) - componentTypes.value = componentTypes.value.filter((type) => type.id !== id) - showSuccess('Type de composant supprimé') - return { success: true } - } catch (error) { - 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 { - loadingComponentTypes.value = false - } - } - - const getComponentTypes = () => componentTypes.value - const isComponentTypeLoading = () => loadingComponentTypes.value + const { types, loading, loadTypes, createType, updateType, deleteType } = useEntityTypes({ + category: 'COMPONENT', + label: 'composant', + }) return { - componentTypes, - loadingComponentTypes, - loadComponentTypes, - createComponentType, - updateComponentType, - deleteComponentType, - getComponentTypes, - isComponentTypeLoading, + componentTypes: types as Ref, + loadingComponentTypes: loading, + loadComponentTypes: loadTypes, + createComponentType: createType, + updateComponentType: updateType, + deleteComponentType: deleteType, + getComponentTypes: () => types.value as ComponentType[], + isComponentTypeLoading: () => loading.value, } } diff --git a/app/composables/useEntityTypes.ts b/app/composables/useEntityTypes.ts new file mode 100644 index 0000000..f6dfc21 --- /dev/null +++ b/app/composables/useEntityTypes.ts @@ -0,0 +1,171 @@ +/** + * Generic entity types composable. + * + * Replaces useComponentTypes, usePieceTypes, useProductTypes which were + * 95%+ identical (only the category string and labels differed). + */ + +import { ref, type Ref } from 'vue' +import { useToast } from './useToast' +import { + listModelTypes, + createModelType, + updateModelType, + deleteModelType, + type ModelType, + type ModelCategory, +} from '~/services/modelTypes' + +export interface EntityType extends ModelType { + description?: string | null +} + +interface EntityTypePayload { + name: string + code?: string + description?: string | null + notes?: string | null + structure?: any +} + +interface EntityTypeResult { + success: boolean + data?: EntityType | EntityType[] + error?: string +} + +interface EntityTypeConfig { + category: ModelCategory + label: string // e.g. 'composant', 'pièce', 'produit' +} + +const generateCodeFromName = (name: string): string => { + return (name || '') + .normalize('NFD') + .replace(/[\u0300-\u036F]/g, '') + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, '') + .replace(/-+/g, '-') || 'type' +} + +// Shared state per category (module-level singletons) +const stateByCategory: Record; loading: Ref }> = {} + +function getOrCreateState(category: ModelCategory) { + if (!stateByCategory[category]) { + stateByCategory[category] = { + types: ref([]), + loading: ref(false), + } + } + return stateByCategory[category] +} + +export function useEntityTypes(config: EntityTypeConfig) { + const { category, label } = config + const { showSuccess, showError } = useToast() + const state = getOrCreateState(category) + + const normalizeItem = (item: ModelType): EntityType => ({ + ...item, + description: item.description ?? item.notes ?? null, + }) + + const loadTypes = async (): Promise => { + state.loading.value = true + try { + const data = await listModelTypes({ + category, + sort: 'name', + dir: 'asc', + limit: 200, + }) + state.types.value = data.items.map(normalizeItem) + return { success: true, data: state.types.value } + } catch (error) { + const err = error as Error & { message?: string } + const message = err?.message || 'Erreur inconnue' + showError(`Impossible de charger les types de ${label}: ${message}`) + return { success: false, error: message } + } finally { + state.loading.value = false + } + } + + const createType = async (payload: EntityTypePayload): Promise => { + state.loading.value = true + try { + const data = await createModelType({ + name: payload.name, + code: payload.code || generateCodeFromName(payload.name), + category, + notes: payload.description ?? payload.notes ?? undefined, + description: payload.description ?? undefined, + structure: payload.structure ?? undefined, + }) + const normalized = normalizeItem(data) + state.types.value.push(normalized) + showSuccess(`Type de ${label} "${data.name}" créé`) + return { success: true, data: normalized } + } catch (error) { + 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 ${label}: ${message}`) + return { success: false, error: message } + } finally { + state.loading.value = false + } + } + + const updateType = async (id: string, payload: EntityTypePayload): Promise => { + state.loading.value = true + try { + const data = await updateModelType(id, { + name: payload.name, + description: payload.description ?? undefined, + notes: payload.notes ?? undefined, + code: payload.code, + structure: payload.structure ?? undefined, + }) + const normalized = normalizeItem(data) + const index = state.types.value.findIndex((t) => t.id === id) + if (index !== -1) state.types.value[index] = normalized + showSuccess(`Type de ${label} "${data.name}" mis à jour`) + return { success: true, data: normalized } + } catch (error) { + 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 ${label}: ${message}`) + return { success: false, error: message } + } finally { + state.loading.value = false + } + } + + const deleteType = async (id: string): Promise => { + state.loading.value = true + try { + await deleteModelType(id) + state.types.value = state.types.value.filter((t) => t.id !== id) + showSuccess(`Type de ${label} supprimé`) + return { success: true } + } catch (error) { + 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 ${label}: ${message}`) + return { success: false, error: message } + } finally { + state.loading.value = false + } + } + + return { + types: state.types, + loading: state.loading, + loadTypes, + createType, + updateType, + deleteType, + } +} diff --git a/app/composables/usePieceTypes.ts b/app/composables/usePieceTypes.ts index b44898f..6347833 100644 --- a/app/composables/usePieceTypes.ts +++ b/app/composables/usePieceTypes.ts @@ -1,164 +1,29 @@ -import { ref } from 'vue' -import { useToast } from './useToast' -import { listModelTypes, createModelType, updateModelType, deleteModelType, type ModelType } from '~/services/modelTypes' +/** + * Backward-compatible wrapper around useEntityTypes. + * Preserves the original API surface (renamed fields) so consumers need no changes. + */ +import { useEntityTypes, type EntityType } from './useEntityTypes' import type { PieceModelStructure } from '~/shared/types/inventory' +import type { Ref } from 'vue' -export interface PieceType extends ModelType { +export interface PieceType extends EntityType { 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([]) -const loadingPieceTypes = ref(false) - export function usePieceTypes() { - const { showSuccess, showError } = useToast() - - const generateCodeFromName = (name: string): string => { - return (name || '') - .normalize('NFD') - .replace(/[\u0300-\u036F]/g, '') - .toLowerCase() - .replace(/[^a-z0-9]+/g, '-') - .replace(/^-+|-+$/g, '') - .replace(/-+/g, '-') || 'type' - } - - const loadPieceTypes = async (): Promise => { - loadingPieceTypes.value = true - try { - const data = await listModelTypes({ - category: 'PIECE', - sort: 'name', - dir: 'asc', - limit: 200, - }) - - pieceTypes.value = data.items.map((item) => ({ - ...item, - structure: item.structure as PieceModelStructure | null, - description: item.description ?? item.notes ?? null, - })) - - return { success: true, data: pieceTypes.value } - } catch (error) { - 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 { - loadingPieceTypes.value = false - } - } - - const createPieceType = async (payload: PieceTypePayload): Promise => { - loadingPieceTypes.value = true - try { - const data = await createModelType({ - name: payload.name, - code: payload.code || generateCodeFromName(payload.name), - category: 'PIECE', - notes: payload.description ?? payload.notes ?? undefined, - description: payload.description ?? undefined, - structure: payload.structure ?? undefined, - }) - - const normalized: PieceType = { - ...data, - structure: data.structure as PieceModelStructure | null, - description: data.description ?? data.notes ?? null, - } - - pieceTypes.value.push(normalized) - showSuccess(`Type de pièce "${data.name}" créé`) - - return { success: true, data: normalized } - } catch (error) { - 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 { - loadingPieceTypes.value = false - } - } - - const updatePieceType = async (id: string, payload: PieceTypePayload): Promise => { - loadingPieceTypes.value = true - try { - const data = await updateModelType(id, { - name: payload.name, - description: payload.description ?? undefined, - notes: payload.notes ?? undefined, - code: payload.code, - structure: payload.structure ?? undefined, - }) - - const normalized: PieceType = { - ...data, - structure: data.structure as PieceModelStructure | null, - description: data.description ?? data.notes ?? null, - } - - 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 } - } catch (error) { - 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 { - loadingPieceTypes.value = false - } - } - - const deletePieceType = async (id: string): Promise => { - loadingPieceTypes.value = true - try { - await deleteModelType(id) - pieceTypes.value = pieceTypes.value.filter((type) => type.id !== id) - showSuccess('Type de pièce supprimé') - return { success: true } - } catch (error) { - 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 { - loadingPieceTypes.value = false - } - } - - const getPieceTypes = () => pieceTypes.value - const isPieceTypeLoading = () => loadingPieceTypes.value + const { types, loading, loadTypes, createType, updateType, deleteType } = useEntityTypes({ + category: 'PIECE', + label: 'pièce', + }) return { - pieceTypes, - loadingPieceTypes, - loadPieceTypes, - createPieceType, - updatePieceType, - deletePieceType, - getPieceTypes, - isPieceTypeLoading, + pieceTypes: types as Ref, + loadingPieceTypes: loading, + loadPieceTypes: loadTypes, + createPieceType: createType, + updatePieceType: updateType, + deletePieceType: deleteType, + getPieceTypes: () => types.value as PieceType[], + isPieceTypeLoading: () => loading.value, } } diff --git a/app/composables/useProductTypes.ts b/app/composables/useProductTypes.ts index 7489fe7..5c8904f 100644 --- a/app/composables/useProductTypes.ts +++ b/app/composables/useProductTypes.ts @@ -1,159 +1,27 @@ -import { ref } from 'vue' -import { useToast } from './useToast' -import { listModelTypes, createModelType, updateModelType, deleteModelType, type ModelType } from '~/services/modelTypes' +/** + * Backward-compatible wrapper around useEntityTypes. + * Preserves the original API surface (renamed fields) so consumers need no changes. + */ +import { useEntityTypes, type EntityType } from './useEntityTypes' import type { ProductModelStructure } from '~/shared/types/inventory' +import type { Ref } from 'vue' -export interface ProductType extends ModelType { +export interface ProductType extends EntityType { 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([]) -const loadingProductTypes = ref(false) - export function useProductTypes() { - const { showSuccess, showError } = useToast() - - const generateCodeFromName = (name: string): string => { - return (name || '') - .normalize('NFD') - .replace(/[\u0300-\u036F]/g, '') - .toLowerCase() - .replace(/[^a-z0-9]+/g, '-') - .replace(/^-+|-+$/g, '') - .replace(/-+/g, '-') || 'type' - } - - const loadProductTypes = async (): Promise => { - loadingProductTypes.value = true - try { - const data = await listModelTypes({ - category: 'PRODUCT', - sort: 'name', - dir: 'asc', - limit: 200, - }) - - 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 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 { - loadingProductTypes.value = false - } - } - - const createProductType = async (payload: ProductTypePayload): Promise => { - loadingProductTypes.value = true - try { - const data = await createModelType({ - name: payload.name, - code: payload.code || generateCodeFromName(payload.name), - category: 'PRODUCT', - notes: payload.description ?? payload.notes ?? undefined, - description: payload.description ?? undefined, - structure: payload.structure ?? undefined, - }) - - const normalized: ProductType = { - ...data, - structure: data.structure as ProductModelStructure | null, - description: data.description ?? data.notes ?? null, - } - - productTypes.value.push(normalized) - showSuccess(`Type de produit "${data.name}" créé`) - - return { success: true, data: normalized } - } catch (error) { - 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 { - loadingProductTypes.value = false - } - } - - const updateProductType = async (id: string, payload: ProductTypePayload): Promise => { - loadingProductTypes.value = true - try { - const data = await updateModelType(id, { - name: payload.name, - description: payload.description ?? undefined, - notes: payload.notes ?? undefined, - code: payload.code, - structure: payload.structure ?? undefined, - }) - - 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) - if (index !== -1) { - productTypes.value[index] = normalized - } - showSuccess(`Type de produit "${data.name}" mis à jour`) - - return { success: true, data: normalized } - } catch (error) { - 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 { - loadingProductTypes.value = false - } - } - - const deleteProductType = async (id: string): Promise => { - loadingProductTypes.value = true - try { - await deleteModelType(id) - productTypes.value = productTypes.value.filter((type) => type.id !== id) - showSuccess('Type de produit supprimé') - return { success: true } - } catch (error) { - 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 { - loadingProductTypes.value = false - } - } + const { types, loading, loadTypes, createType, updateType, deleteType } = useEntityTypes({ + category: 'PRODUCT', + label: 'produit', + }) return { - productTypes, - loadingProductTypes, - loadProductTypes, - createProductType, - updateProductType, - deleteProductType, + productTypes: types as Ref, + loadingProductTypes: loading, + loadProductTypes: loadTypes, + createProductType: createType, + updateProductType: updateType, + deleteProductType: deleteType, } }