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 <noreply@anthropic.com>
This commit is contained in:
Matthieu
2026-02-09 11:13:39 +01:00
parent 399ec1f7b4
commit a6664ce9a2
4 changed files with 226 additions and 457 deletions

View File

@@ -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<ComponentType[]>([])
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<ComponentTypeResult> => {
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<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 ?? 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<ComponentTypeResult> => {
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<ComponentTypeResult> => {
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<ComponentType[]>,
loadingComponentTypes: loading,
loadComponentTypes: loadTypes,
createComponentType: createType,
updateComponentType: updateType,
deleteComponentType: deleteType,
getComponentTypes: () => types.value as ComponentType[],
isComponentTypeLoading: () => loading.value,
}
}

View File

@@ -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<string, { types: Ref<EntityType[]>; loading: Ref<boolean> }> = {}
function getOrCreateState(category: ModelCategory) {
if (!stateByCategory[category]) {
stateByCategory[category] = {
types: ref<EntityType[]>([]),
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<EntityTypeResult> => {
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<EntityTypeResult> => {
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<EntityTypeResult> => {
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<EntityTypeResult> => {
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,
}
}

View File

@@ -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<PieceType[]>([])
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<PieceTypeResult> => {
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<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 ?? 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<PieceTypeResult> => {
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<PieceTypeResult> => {
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<PieceType[]>,
loadingPieceTypes: loading,
loadPieceTypes: loadTypes,
createPieceType: createType,
updatePieceType: updateType,
deletePieceType: deleteType,
getPieceTypes: () => types.value as PieceType[],
isPieceTypeLoading: () => loading.value,
}
}

View File

@@ -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<ProductType[]>([])
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<ProductTypeResult> => {
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<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 ?? 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<ProductTypeResult> => {
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<ProductTypeResult> => {
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<ProductType[]>,
loadingProductTypes: loading,
loadProductTypes: loadTypes,
createProductType: createType,
updateProductType: updateType,
deleteProductType: deleteType,
}
}