feat: add product catalogue and product-aware UI

- introduce product catalogue pages, management view entries and shared product composables\n- wire product selection into component/piece flows and machine skeleton requirements\n- display linked product metadata and documents across machine, component and piece views\n- generalize model type tooling to handle PRODUCT category
This commit is contained in:
Matthieu
2025-11-05 15:35:02 +01:00
parent 3af6c50892
commit d860f24e69
42 changed files with 6052 additions and 142 deletions

View File

@@ -60,6 +60,11 @@ export function useDocuments () {
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 })
@@ -140,6 +145,7 @@ export function useDocuments () {
loadDocumentsByMachine,
loadDocumentsByComponent,
loadDocumentsByPiece,
loadDocumentsByProduct,
uploadDocuments,
deleteDocument
}

View File

@@ -5,6 +5,20 @@ import { useApi } from './useApi'
const machineTypes = ref([])
const loading = ref(false)
const normalizeRequirementList = (value) => (Array.isArray(value) ? value : [])
const normalizeMachineType = (type) => {
if (!type || typeof type !== 'object') {
return type
}
return {
...type,
componentRequirements: normalizeRequirementList(type.componentRequirements),
pieceRequirements: normalizeRequirementList(type.pieceRequirements),
productRequirements: normalizeRequirementList(type.productRequirements),
}
}
export function useMachineTypesApi () {
const { showSuccess, showError, showInfo } = useToast()
const { get, post, patch, delete: del } = useApi()
@@ -14,7 +28,9 @@ export function useMachineTypesApi () {
try {
const result = await get('/types/machines')
if (result.success) {
machineTypes.value = result.data
machineTypes.value = Array.isArray(result.data)
? result.data.map(normalizeMachineType)
: []
showInfo(`Chargement de ${machineTypes.value.length} type(s) de machine réussi`)
}
} catch (error) {
@@ -29,7 +45,7 @@ export function useMachineTypesApi () {
try {
const result = await post('/types/machines', typeData)
if (result.success) {
machineTypes.value.push(result.data)
machineTypes.value.push(normalizeMachineType(result.data))
showSuccess(`Type de machine "${typeData.name}" créé avec succès`)
}
return result
@@ -46,9 +62,10 @@ export function useMachineTypesApi () {
try {
const result = await patch(`/types/machines/${id}`, typeData)
if (result.success) {
const normalized = normalizeMachineType(result.data)
const index = machineTypes.value.findIndex(type => type.id === id)
if (index !== -1) {
machineTypes.value[index] = result.data
machineTypes.value[index] = normalized
}
showSuccess(`Type de machine "${typeData.name}" mis à jour avec succès`)
}
@@ -91,7 +108,7 @@ export function useMachineTypesApi () {
const result = await get(`/types/machines/${id}`)
if (result.success) {
// Ajouter au cache local
machineTypes.value.push(result.data)
machineTypes.value.push(normalizeMachineType(result.data))
}
return result
} catch (error) {

View File

@@ -0,0 +1,132 @@
import { ref } from 'vue'
import { useToast } from './useToast'
import { listModelTypes, createModelType, updateModelType, deleteModelType } from '~/services/modelTypes'
const productTypes = ref([])
const loadingProductTypes = ref(false)
export function useProductTypes () {
const { showSuccess, showError } = useToast()
const generateCodeFromName = (name) => {
return (name || '')
.normalize('NFD')
.replace(/[\u0300-\u036F]/g, '')
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
.replace(/-+/g, '-') || 'type'
}
const loadProductTypes = async () => {
loadingProductTypes.value = true
try {
const data = await listModelTypes({
category: 'PRODUCT',
sort: 'name',
dir: 'asc',
limit: 200,
})
productTypes.value = data.items.map(item => ({
...item,
description: item.description ?? item.notes ?? null,
}))
return { success: true, data: productTypes.value }
} catch (error) {
const message = error?.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) => {
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,
})
const normalized = {
...data,
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 message = error?.data?.message || error?.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, payload) => {
loadingProductTypes.value = true
try {
const data = await updateModelType(id, {
name: payload.name,
description: payload.description,
notes: payload.notes,
code: payload.code,
structure: payload.structure,
})
const normalized = {
...data,
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 message = error?.data?.message || error?.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) => {
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 message = error?.data?.message || error?.message || 'Erreur inconnue'
showError(`Erreur lors de la suppression du type de produit: ${message}`)
return { success: false, error: message }
} finally {
loadingProductTypes.value = false
}
}
return {
productTypes,
loadingProductTypes,
loadProductTypes,
createProductType,
updateProductType,
deleteProductType,
}
}

View File

@@ -0,0 +1,184 @@
import { ref } from 'vue'
import { useToast } from './useToast'
import { useApi } from './useApi'
const products = ref([])
const total = ref(0)
const loading = ref(false)
const loaded = ref(false)
const error = ref(null)
const replaceInCache = (item) => {
if (!item?.id) {
return false
}
const index = products.value.findIndex((product) => product.id === item.id)
if (index === -1) {
products.value.unshift(item)
return true
}
const clone = products.value.slice()
clone[index] = item
products.value = clone
return false
}
export function useProducts () {
const { showError } = useToast()
const { get, post, patch, delete: del } = useApi()
const loadProducts = async (options = {}) => {
if (loading.value) {
return {
success: true,
data: { items: products.value, total: total.value },
}
}
if (loaded.value && !options.force) {
return {
success: true,
data: { items: products.value, total: total.value },
}
}
loading.value = true
error.value = null
try {
const result = await get('/products?limit=100')
if (result.success) {
const items = Array.isArray(result.data?.items) ? result.data.items : []
products.value = items
total.value = typeof result.data?.total === 'number' ? result.data.total : items.length
loaded.value = true
} else if (result.error) {
error.value = result.error
showError(`Impossible de charger les produits: ${result.error}`)
}
return result
} catch (err) {
console.error('Erreur lors du chargement des produits:', err)
const message = err?.message ?? 'Erreur inconnue'
error.value = message
showError(`Impossible de charger les produits: ${message}`)
return { success: false, error: message }
} finally {
loading.value = false
}
}
const createProduct = async (payload) => {
loading.value = true
error.value = null
try {
const result = await post('/products', payload)
if (result.success && result.data) {
const added = replaceInCache(result.data)
if (added) {
total.value += 1
}
} else if (result.error) {
error.value = result.error
showError(result.error)
}
return result
} catch (err) {
console.error('Erreur lors de la création du produit:', err)
const message = err?.message ?? 'Erreur inconnue'
error.value = message
showError(message)
return { success: false, error: message }
} finally {
loading.value = false
}
}
const updateProduct = async (id, payload) => {
loading.value = true
error.value = null
try {
const result = await patch(`/products/${id}`, payload)
if (result.success && result.data) {
replaceInCache(result.data)
} else if (result.error) {
error.value = result.error
showError(result.error)
}
return result
} catch (err) {
console.error('Erreur lors de la mise à jour du produit:', err)
const message = err?.message ?? 'Erreur inconnue'
error.value = message
showError(message)
return { success: false, error: message }
} finally {
loading.value = false
}
}
const deleteProduct = async (id) => {
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)
} else if (result.error) {
error.value = result.error
showError(result.error)
}
return result
} catch (err) {
console.error('Erreur lors de la suppression du produit:', err)
const message = err?.message ?? 'Erreur inconnue'
error.value = message
showError(message)
return { success: false, error: message }
} finally {
loading.value = false
}
}
const getProduct = async (id, options = {}) => {
if (!options.force) {
const cached = products.value.find((product) => product.id === id)
if (cached) {
return { success: true, data: cached }
}
}
try {
const result = await get(`/products/${id}`)
if (result.success && result.data) {
replaceInCache(result.data)
}
return result
} catch (err) {
console.error('Erreur lors du chargement du produit:', err)
const message = err?.message ?? 'Erreur inconnue'
return { success: false, error: message }
}
}
const clearProductsCache = () => {
products.value = []
total.value = 0
loaded.value = false
error.value = null
}
return {
products,
total,
loading,
loaded,
error,
loadProducts,
createProduct,
updateProduct,
deleteProduct,
getProduct,
clearProductsCache,
}
}