diff --git a/app/components/ComponentItem.vue b/app/components/ComponentItem.vue
index 15c3566..fd0701e 100644
--- a/app/components/ComponentItem.vue
+++ b/app/components/ComponentItem.vue
@@ -109,6 +109,7 @@
v-if="isEditMode"
class="w-full"
:model-value="componentConstructeurIds"
+ :initial-options="componentConstructeursDisplay"
@update:model-value="handleConstructeurChange"
/>
diff --git a/app/components/ConstructeurSelect.vue b/app/components/ConstructeurSelect.vue
index e2cb60a..7bf19d8 100644
--- a/app/components/ConstructeurSelect.vue
+++ b/app/components/ConstructeurSelect.vue
@@ -142,13 +142,22 @@ const props = defineProps({
type: String,
default: 'Sélectionner ou créer un fournisseur...',
},
+ initialOptions: {
+ type: Array as PropType,
+ default: () => [],
+ },
})
const emit = defineEmits<{
(e: 'update:modelValue', value: string[]): void
}>()
-const { constructeurs, searchConstructeurs, createConstructeur } = useConstructeurs()
+const {
+ constructeurs,
+ searchConstructeurs,
+ createConstructeur,
+ ensureConstructeurs,
+} = useConstructeurs()
const searchTerm = ref('')
const openDropdown = ref(false)
const openCreateModal = ref(false)
@@ -168,8 +177,15 @@ const uniqueOptions = (items: ConstructeurSummary[] = []) => {
return Array.from(seen.values())
}
+const normalizedInitialOptions = computed(() =>
+ uniqueOptions((props.initialOptions as ConstructeurSummary[]) || []),
+)
+
const applyOptions = (items: ConstructeurSummary[] = []) => {
- const normalized = uniqueOptions(items)
+ const normalized = uniqueOptions([
+ ...normalizedInitialOptions.value,
+ ...items,
+ ])
const limited = normalized.slice(0, 10)
selectedIds.value.forEach((id) => {
@@ -186,7 +202,10 @@ const applyOptions = (items: ConstructeurSummary[] = []) => {
}
})
- options.value = uniqueOptions(limited)
+ options.value = uniqueOptions([
+ ...normalizedInitialOptions.value,
+ ...limited,
+ ])
}
const createForm = ref({
@@ -197,6 +216,9 @@ const createForm = ref({
const optionLookup = computed(() => {
const map = new Map()
+ normalizedInitialOptions.value.forEach((item) => {
+ map.set(item.id, item)
+ })
constructeurs.value.forEach((item: ConstructeurSummary) => {
map.set(item.id, item)
})
@@ -336,7 +358,10 @@ watch(
}
const missing = ids.some((id) => !optionLookup.value.get(id))
if (missing) {
- await ensureOptionsLoaded(true)
+ const fetched = await ensureConstructeurs(ids)
+ if (fetched.length) {
+ applyOptions([...options.value, ...fetched])
+ }
}
},
{ immediate: true },
@@ -353,6 +378,14 @@ watch(
{ immediate: true },
)
+watch(
+ normalizedInitialOptions,
+ () => {
+ applyOptions(options.value)
+ },
+ { immediate: true },
+)
+
onMounted(() => {
window.addEventListener('click', clickHandler)
ensureOptionsLoaded()
diff --git a/app/components/PieceItem.vue b/app/components/PieceItem.vue
index a3be067..9454961 100644
--- a/app/components/PieceItem.vue
+++ b/app/components/PieceItem.vue
@@ -100,6 +100,7 @@
v-else
class="w-full"
:model-value="pieceConstructeurIds"
+ :initial-options="pieceConstructeursDisplay"
placeholder="Sélectionner un ou plusieurs fournisseurs..."
@update:model-value="handleConstructeurChange"
/>
diff --git a/app/composables/useApi.js b/app/composables/useApi.js
index 24276e2..b2945e7 100644
--- a/app/composables/useApi.js
+++ b/app/composables/useApi.js
@@ -29,10 +29,27 @@ export function useApi () {
clearTimeout(timeoutId)
if (response.ok) {
- const data = await response.json()
+ let data = null
+ if (response.status !== 204) {
+ const contentType = response.headers.get('content-type') || ''
+ if (contentType.includes('application/json')) {
+ const text = await response.text()
+ data = text ? JSON.parse(text) : null
+ } else {
+ const text = await response.text()
+ data = text || null
+ }
+ }
return { success: true, data }
} else {
- const errorData = await response.json().catch(() => ({}))
+ const contentType = response.headers.get('content-type') || ''
+ let errorData = {}
+ if (contentType.includes('application/json')) {
+ errorData = await response.json().catch(() => ({}))
+ } else {
+ const text = await response.text().catch(() => '')
+ errorData = text ? { message: text } : {}
+ }
const errorMessage = errorData.message || `Erreur ${response.status}: ${response.statusText}`
showError(errorMessage)
return { success: false, error: errorMessage, status: response.status }
diff --git a/app/composables/useComposants.js b/app/composables/useComposants.js
index 19b33ea..5599ede 100644
--- a/app/composables/useComposants.js
+++ b/app/composables/useComposants.js
@@ -1,7 +1,8 @@
import { ref } from 'vue'
import { useToast } from './useToast'
import { useApi } from './useApi'
-import { buildConstructeurRequestPayload } from '~/shared/constructeurUtils'
+import { buildConstructeurRequestPayload, uniqueConstructeurIds } from '~/shared/constructeurUtils'
+import { useConstructeurs } from './useConstructeurs'
const composants = ref([])
const loading = ref(false)
@@ -9,13 +10,38 @@ const loading = ref(false)
export function useComposants () {
const { showSuccess, showError, showInfo } = useToast()
const { get, post, patch, delete: del } = useApi()
+ const { ensureConstructeurs } = useConstructeurs()
+
+ const withResolvedConstructeurs = async (composant) => {
+ if (!composant || typeof composant !== 'object') {
+ return composant
+ }
+ const ids = uniqueConstructeurIds(
+ composant.constructeurIds,
+ composant.constructeurs,
+ composant.constructeur,
+ )
+ const hasConstructeurs =
+ Array.isArray(composant.constructeurs) && composant.constructeurs.length > 0
+
+ if (ids.length && !hasConstructeurs) {
+ const resolved = await ensureConstructeurs(ids)
+ if (resolved.length) {
+ composant.constructeurs = resolved
+ composant.constructeurIds = ids
+ }
+ }
+ return composant
+ }
const loadComposants = async () => {
loading.value = true
try {
const result = await get('/composants')
if (result.success) {
- composants.value = result.data
+ const items = Array.isArray(result.data) ? result.data : []
+ const enrichedItems = await Promise.all(items.map((item) => withResolvedConstructeurs(item)))
+ composants.value = enrichedItems
showInfo(`Chargement de ${composants.value.length} composant(s) réussi`)
}
} catch (error) {
@@ -30,7 +56,8 @@ const loadComposants = async () => {
try {
const result = await post('/composants', buildConstructeurRequestPayload(composantData))
if (result.success) {
- composants.value.push(result.data)
+ const enriched = await withResolvedConstructeurs(result.data)
+ composants.value.push(enriched)
const displayName = result.data?.name
|| composantData?.definition?.name
|| composantData?.name
@@ -51,7 +78,7 @@ const loadComposants = async () => {
try {
const result = await patch(`/composants/${id}`, buildConstructeurRequestPayload(composantData))
if (result.success) {
- const updated = result.data
+ const updated = await withResolvedConstructeurs(result.data)
const index = composants.value.findIndex(comp => comp.id === id)
if (index !== -1) {
composants.value[index] = updated
diff --git a/app/composables/useConstructeurs.js b/app/composables/useConstructeurs.js
index 4a6cd6b..025e5ed 100644
--- a/app/composables/useConstructeurs.js
+++ b/app/composables/useConstructeurs.js
@@ -5,6 +5,42 @@ import { useToast } from './useToast'
const constructeurs = ref([])
const loading = ref(false)
+const uniqueConstructeurs = (items = []) => {
+ const map = new Map()
+ items.forEach((item) => {
+ if (item && typeof item === 'object' && typeof item.id === 'string') {
+ map.set(item.id, item)
+ }
+ })
+ return Array.from(map.values())
+}
+
+const normalizeIds = (ids = []) => {
+ if (!Array.isArray(ids)) {
+ return []
+ }
+ return Array.from(
+ new Set(
+ ids
+ .map((value) => (typeof value === 'string' ? value.trim() : ''))
+ .filter((value) => value.length > 0),
+ ),
+ )
+}
+
+const upsertConstructeurs = (items = []) => {
+ if (!Array.isArray(items) || !items.length) {
+ return
+ }
+ const merged = uniqueConstructeurs([...constructeurs.value, ...items])
+ constructeurs.value = merged
+}
+
+const getIndexedConstructeur = (id) =>
+ constructeurs.value.find((item) => item && item.id === id) || null
+
+const pendingFetches = new Map()
+
export function useConstructeurs () {
const { get, post, patch, delete: del } = useApi()
const { showSuccess, showError } = useToast()
@@ -15,7 +51,8 @@ export function useConstructeurs () {
const query = search ? `?search=${encodeURIComponent(search)}` : ''
const result = await get(`/constructeurs${query}`)
if (result.success) {
- constructeurs.value = result.data
+ const items = Array.isArray(result.data) ? result.data : []
+ constructeurs.value = uniqueConstructeurs(items)
}
return result
} catch (error) {
@@ -35,7 +72,7 @@ export function useConstructeurs () {
try {
const result = await post('/constructeurs', data)
if (result.success) {
- constructeurs.value = [result.data, ...constructeurs.value]
+ upsertConstructeurs([result.data])
showSuccess(`Fournisseur "${result.data.name}" créé`)
} else if (result.error) {
showError(result.error)
@@ -50,15 +87,65 @@ export function useConstructeurs () {
}
}
+ const ensureConstructeurs = async (ids = []) => {
+ const normalizedIds = normalizeIds(ids)
+ if (!normalizedIds.length) {
+ return []
+ }
+
+ const collected = []
+ const missing = []
+ normalizedIds.forEach((id) => {
+ const existing = getIndexedConstructeur(id)
+ if (existing) {
+ collected.push(existing)
+ } else {
+ missing.push(id)
+ }
+ })
+
+ if (missing.length) {
+ const fetchTasks = missing.map((id) => {
+ const cached = pendingFetches.get(id)
+ if (cached) {
+ return cached
+ }
+ const task = get(`/constructeurs/${id}`)
+ .then((result) => {
+ if (result.success && result.data) {
+ return result.data
+ }
+ return null
+ })
+ .catch((error) => {
+ console.error('Erreur lors du chargement du fournisseur:', error)
+ return null
+ })
+ .finally(() => {
+ pendingFetches.delete(id)
+ })
+ pendingFetches.set(id, task)
+ return task
+ })
+
+ const fetched = await Promise.all(fetchTasks)
+ const validFetched = fetched.filter((item) => item && item.id)
+ if (validFetched.length) {
+ upsertConstructeurs(validFetched)
+ }
+ }
+
+ return normalizedIds
+ .map((id) => getIndexedConstructeur(id))
+ .filter((item) => Boolean(item))
+ }
+
const updateConstructeur = async (id, data) => {
loading.value = true
try {
const result = await patch(`/constructeurs/${id}`, data)
if (result.success) {
- const index = constructeurs.value.findIndex(item => item.id === id)
- if (index !== -1) {
- constructeurs.value[index] = result.data
- }
+ upsertConstructeurs([result.data])
showSuccess(`Fournisseur "${result.data.name}" mis à jour`)
} else if (result.error) {
showError(result.error)
@@ -93,7 +180,7 @@ export function useConstructeurs () {
}
}
- const getConstructeurById = id => constructeurs.value.find(item => item.id === id)
+ const getConstructeurById = (id) => getIndexedConstructeur(id)
return {
constructeurs,
@@ -103,6 +190,7 @@ export function useConstructeurs () {
createConstructeur,
updateConstructeur,
deleteConstructeur,
- getConstructeurById
+ getConstructeurById,
+ ensureConstructeurs,
}
}
diff --git a/app/composables/usePieces.js b/app/composables/usePieces.js
index 152b070..aa0112e 100644
--- a/app/composables/usePieces.js
+++ b/app/composables/usePieces.js
@@ -1,7 +1,8 @@
import { ref } from 'vue'
import { useToast } from './useToast'
import { useApi } from './useApi'
-import { buildConstructeurRequestPayload } from '~/shared/constructeurUtils'
+import { buildConstructeurRequestPayload, uniqueConstructeurIds } from '~/shared/constructeurUtils'
+import { useConstructeurs } from './useConstructeurs'
const pieces = ref([])
const loading = ref(false)
@@ -9,13 +10,38 @@ const loading = ref(false)
export function usePieces () {
const { showSuccess, showError, showInfo } = useToast()
const { get, post, patch, delete: del } = useApi()
+ const { ensureConstructeurs } = useConstructeurs()
+
+ const withResolvedConstructeurs = async (piece) => {
+ if (!piece || typeof piece !== 'object') {
+ return piece
+ }
+ const ids = uniqueConstructeurIds(
+ piece.constructeurIds,
+ piece.constructeurs,
+ piece.constructeur,
+ )
+ const hasConstructeurs =
+ Array.isArray(piece.constructeurs) && piece.constructeurs.length > 0
+
+ if (ids.length && !hasConstructeurs) {
+ const resolved = await ensureConstructeurs(ids)
+ if (resolved.length) {
+ piece.constructeurs = resolved
+ piece.constructeurIds = ids
+ }
+ }
+ return piece
+ }
const loadPieces = async () => {
loading.value = true
try {
const result = await get('/pieces')
if (result.success) {
- pieces.value = result.data
+ const items = Array.isArray(result.data) ? result.data : []
+ const enrichedItems = await Promise.all(items.map((item) => withResolvedConstructeurs(item)))
+ pieces.value = enrichedItems
showInfo(`Chargement de ${pieces.value.length} pièce(s) réussi`)
}
} catch (error) {
@@ -30,7 +56,8 @@ export function usePieces () {
try {
const result = await post('/pieces', buildConstructeurRequestPayload(pieceData))
if (result.success) {
- pieces.value.push(result.data)
+ const enriched = await withResolvedConstructeurs(result.data)
+ pieces.value.push(enriched)
const displayName = result.data?.name
|| pieceData?.definition?.name
|| pieceData?.name
@@ -51,7 +78,7 @@ export function usePieces () {
try {
const result = await patch(`/pieces/${id}`, buildConstructeurRequestPayload(pieceData))
if (result.success) {
- const updated = result.data
+ const updated = await withResolvedConstructeurs(result.data)
const index = pieces.value.findIndex(piece => piece.id === id)
if (index !== -1) {
pieces.value[index] = updated
diff --git a/app/composables/useProducts.js b/app/composables/useProducts.js
index 59207ef..9a90ed7 100644
--- a/app/composables/useProducts.js
+++ b/app/composables/useProducts.js
@@ -1,6 +1,8 @@
import { ref } from 'vue'
import { useToast } from './useToast'
import { useApi } from './useApi'
+import { buildConstructeurRequestPayload, uniqueConstructeurIds } from '~/shared/constructeurUtils'
+import { useConstructeurs } from './useConstructeurs'
const products = ref([])
const total = ref(0)
@@ -26,6 +28,29 @@ const replaceInCache = (item) => {
export function useProducts () {
const { showError } = useToast()
const { get, post, patch, delete: del } = useApi()
+ const { ensureConstructeurs } = useConstructeurs()
+
+ const withResolvedConstructeurs = async (product) => {
+ if (!product || typeof product !== 'object') {
+ return product
+ }
+ const ids = uniqueConstructeurIds(
+ product.constructeurIds,
+ product.constructeurs,
+ product.constructeur,
+ )
+ const hasConstructeurs =
+ Array.isArray(product.constructeurs) && product.constructeurs.length > 0
+
+ if (ids.length && !hasConstructeurs) {
+ const resolved = await ensureConstructeurs(ids)
+ if (resolved.length) {
+ product.constructeurs = resolved
+ product.constructeurIds = ids
+ }
+ }
+ return product
+ }
const loadProducts = async (options = {}) => {
if (loading.value) {
@@ -47,7 +72,8 @@ export function useProducts () {
const result = await get('/products?limit=100')
if (result.success) {
const items = Array.isArray(result.data?.items) ? result.data.items : []
- products.value = items
+ const enrichedItems = await Promise.all(items.map((item) => withResolvedConstructeurs(item)))
+ products.value = enrichedItems
total.value = typeof result.data?.total === 'number' ? result.data.total : items.length
loaded.value = true
} else if (result.error) {
@@ -67,12 +93,14 @@ export function useProducts () {
}
const createProduct = async (payload) => {
+ const normalizedPayload = buildConstructeurRequestPayload(payload)
loading.value = true
error.value = null
try {
- const result = await post('/products', payload)
+ const result = await post('/products', normalizedPayload)
if (result.success && result.data) {
- const added = replaceInCache(result.data)
+ const enriched = await withResolvedConstructeurs(result.data)
+ const added = replaceInCache(enriched)
if (added) {
total.value += 1
}
@@ -93,12 +121,14 @@ export function useProducts () {
}
const updateProduct = async (id, payload) => {
+ const normalizedPayload = buildConstructeurRequestPayload(payload)
loading.value = true
error.value = null
try {
- const result = await patch(`/products/${id}`, payload)
+ const result = await patch(`/products/${id}`, normalizedPayload)
if (result.success && result.data) {
- replaceInCache(result.data)
+ const enriched = await withResolvedConstructeurs(result.data)
+ replaceInCache(enriched)
} else if (result.error) {
error.value = result.error
showError(result.error)
@@ -141,9 +171,10 @@ export function useProducts () {
}
const getProduct = async (id, options = {}) => {
- if (!options.force) {
+ const shouldForce = !!options.force
+ if (!shouldForce) {
const cached = products.value.find((product) => product.id === id)
- if (cached) {
+ if (cached && Array.isArray(cached.constructeurs) && cached.constructeurs.length > 0) {
return { success: true, data: cached }
}
}
@@ -151,7 +182,9 @@ export function useProducts () {
try {
const result = await get(`/products/${id}`)
if (result.success && result.data) {
- replaceInCache(result.data)
+ const enriched = await withResolvedConstructeurs(result.data)
+ replaceInCache(enriched)
+ return { success: true, data: enriched }
}
return result
} catch (err) {
diff --git a/app/pages/component/[id]/edit.vue b/app/pages/component/[id]/edit.vue
index 21af22f..bc7d32f 100644
--- a/app/pages/component/[id]/edit.vue
+++ b/app/pages/component/[id]/edit.vue
@@ -102,6 +102,7 @@
class="w-full"
:disabled="saving"
placeholder="Rechercher un ou plusieurs fournisseurs..."
+ :initial-options="component?.constructeurs || []"
/>
@@ -403,6 +404,7 @@ import { useCustomFields } from '~/composables/useCustomFields'
import { useApi } from '~/composables/useApi'
import { useToast } from '~/composables/useToast'
import { useDocuments } from '~/composables/useDocuments'
+import { useConstructeurs } from '~/composables/useConstructeurs'
import { formatStructurePreview, normalizeStructureForEditor } from '~/shared/modelUtils'
import { uniqueConstructeurIds } from '~/shared/constructeurUtils'
import type { ComponentModelStructure } from '~/shared/types/inventory'
@@ -432,6 +434,7 @@ const router = useRouter()
const { get } = useApi()
const { componentTypes, loadComponentTypes } = useComponentTypes()
const { updateComposant } = useComposants()
+const { ensureConstructeurs } = useConstructeurs()
const { upsertCustomFieldValue, updateCustomFieldValue } = useCustomFields()
const toast = useToast()
const { loadDocumentsByComponent, uploadDocuments, deleteDocument } = useDocuments()
@@ -658,6 +661,9 @@ watch(
currentComponent.constructeur ? [currentComponent.constructeur] : [],
)
editionForm.prix = currentComponent.prix !== null && currentComponent.prix !== undefined ? String(currentComponent.prix) : ''
+ if (editionForm.constructeurIds.length) {
+ void ensureConstructeurs(editionForm.constructeurIds)
+ }
customFieldInputs.value = buildCustomFieldInputs(
currentStructure,
diff --git a/app/pages/machine/[id].vue b/app/pages/machine/[id].vue
index 9a575d6..f1e00dd 100644
--- a/app/pages/machine/[id].vue
+++ b/app/pages/machine/[id].vue
@@ -144,6 +144,7 @@
class="w-full"
:key="machine.value?.id"
:model-value="machineConstructeurIds"
+ :initial-options="machineConstructeursDisplay"
placeholder="Rechercher un ou plusieurs fournisseurs..."
@update:modelValue="handleMachineConstructeurChange"
/>
diff --git a/app/pages/pieces-catalog.vue b/app/pages/pieces-catalog.vue
index 8c4923d..1ca85a0 100644
--- a/app/pages/pieces-catalog.vue
+++ b/app/pages/pieces-catalog.vue
@@ -93,25 +93,48 @@
Aperçu |
Nom |
Référence |
+ Fournisseurs |
Type de pièce |
Actions |
-
+
|
|
- {{ piece.name || 'Pièce sans nom' }} |
- {{ piece.reference || '—' }} |
- {{ resolvePieceType(piece) }} |
+ {{ row.piece.name || 'Pièce sans nom' }} |
+ {{ row.piece.reference || '—' }} |
+
+
+
+ {{ supplier }}
+
+
+ +{{ row.suppliers.overflow }}
+
+
+ —
+ |
+ {{ resolvePieceType(row.piece) }} |
Modifier
@@ -120,7 +143,7 @@
type="button"
class="btn btn-error btn-xs"
:disabled="loadingPieces"
- @click="handleDeletePiece(piece)"
+ @click="handleDeletePiece(row.piece)"
>
Supprimer
@@ -193,6 +216,88 @@ const resolvePieceType = (piece: Record) => {
return '—'
}
+const MAX_VISIBLE_SUPPLIERS = 3
+
+const resolvePieceSuppliers = (piece: Record) => {
+ const names: string[] = []
+ const seen = new Set()
+
+ const pushName = (maybeName: unknown) => {
+ if (typeof maybeName !== 'string') {
+ return
+ }
+ const normalized = maybeName.trim().replace(/\s+/g, ' ')
+ if (!normalized.length) {
+ return
+ }
+ const key = normalized.toLowerCase()
+ if (seen.has(key)) {
+ return
+ }
+ seen.add(key)
+ names.push(normalized)
+ }
+
+ const collectConstructeurs = (value: unknown): void => {
+ if (!value) {
+ return
+ }
+ if (Array.isArray(value)) {
+ value.forEach(collectConstructeurs)
+ return
+ }
+ if (typeof value === 'string') {
+ pushName(value)
+ return
+ }
+ if (typeof value === 'object') {
+ const record = value as Record
+ pushName(record?.name ?? record?.label ?? record?.companyName ?? record?.company ?? null)
+ if (record?.constructeur) {
+ collectConstructeurs(record.constructeur)
+ }
+ if (Array.isArray(record?.constructeurs)) {
+ collectConstructeurs(record.constructeurs)
+ }
+ }
+ }
+
+ const collectFromLabel = (value: unknown): void => {
+ if (typeof value !== 'string') {
+ return
+ }
+ value
+ .split(/[,;\\/•·|]+/)
+ .map((part) => part.trim())
+ .filter(Boolean)
+ .forEach(pushName)
+ }
+
+ collectConstructeurs(piece?.constructeurs)
+ collectConstructeurs(piece?.constructeur)
+ collectConstructeurs(piece?.product?.constructeurs)
+ collectConstructeurs(piece?.product?.constructeur)
+
+ collectFromLabel(piece?.constructeursLabel)
+ collectFromLabel(piece?.supplierLabel)
+ collectFromLabel(piece?.product?.constructeursLabel)
+ collectFromLabel(piece?.product?.supplierLabel)
+
+ return names
+}
+
+const buildPieceSuppliersDisplay = (piece: Record) => {
+ const suppliers = resolvePieceSuppliers(piece)
+ const visible = suppliers.slice(0, MAX_VISIBLE_SUPPLIERS)
+ const overflow = Math.max(suppliers.length - visible.length, 0)
+ return {
+ suppliers,
+ visible,
+ overflow,
+ tooltip: suppliers.length ? suppliers.join(', ') : '',
+ }
+}
+
const resolveDeleteGuard = (piece: Record) => {
const blockingReasons: string[] = []
const machineLinks = Array.isArray(piece?.machineLinks)
@@ -269,6 +374,13 @@ const visiblePieces = computed(() => {
})
})
+const pieceRows = computed(() =>
+ visiblePieces.value.map((piece) => ({
+ piece,
+ suppliers: buildPieceSuppliersDisplay(piece),
+ })),
+)
+
const handleDeletePiece = async (piece: Record) => {
const { blockingReasons, hasCustomFields } = resolveDeleteGuard(piece)
diff --git a/app/pages/pieces/[id]/edit.vue b/app/pages/pieces/[id]/edit.vue
index 6811205..cacec87 100644
--- a/app/pages/pieces/[id]/edit.vue
+++ b/app/pages/pieces/[id]/edit.vue
@@ -102,6 +102,7 @@
class="w-full"
:disabled="saving"
placeholder="Rechercher un ou plusieurs fournisseurs..."
+ :initial-options="piece?.constructeurs || []"
/>
@@ -393,6 +394,7 @@ import { useCustomFields } from '~/composables/useCustomFields'
import { useApi } from '~/composables/useApi'
import { useToast } from '~/composables/useToast'
import { useDocuments } from '~/composables/useDocuments'
+import { useConstructeurs } from '~/composables/useConstructeurs'
import { getFileIcon } from '~/utils/fileIcons'
import { canPreviewDocument, isImageDocument, isPdfDocument } from '~/utils/documentPreview'
import { formatPieceStructurePreview } from '~/shared/modelUtils'
@@ -425,6 +427,7 @@ const { updatePiece } = usePieces()
const { upsertCustomFieldValue, updateCustomFieldValue } = useCustomFields()
const toast = useToast()
const { loadDocumentsByPiece, uploadDocuments, deleteDocument } = useDocuments()
+const { ensureConstructeurs } = useConstructeurs()
const piece = ref(null)
const loading = ref(true)
@@ -682,6 +685,9 @@ watch(
)
editionForm.prix = currentPiece.prix !== null && currentPiece.prix !== undefined ? String(currentPiece.prix) : ''
editionForm.productId = currentPiece.product?.id || currentPiece.productId || null
+ if (editionForm.constructeurIds.length) {
+ void ensureConstructeurs(editionForm.constructeurIds)
+ }
customFieldInputs.value = buildCustomFieldInputs(
currentType?.structure ?? null,
@@ -719,13 +725,15 @@ const submitEdition = async () => {
? ''
: String(editionForm.prix).trim()
+ const constructeurIds = uniqueConstructeurIds(editionForm.constructeurIds)
+
const payload: Record = {
name: editionForm.name.trim(),
+ constructeurIds,
}
const reference = editionForm.reference.trim()
payload.reference = reference ? reference : null
- payload.constructeurIds = uniqueConstructeurIds(editionForm.constructeurIds)
const selectedProductId =
typeof editionForm.productId === 'string'
diff --git a/app/pages/pieces/create.vue b/app/pages/pieces/create.vue
index 4d57437..5750858 100644
--- a/app/pages/pieces/create.vue
+++ b/app/pages/pieces/create.vue
@@ -485,9 +485,7 @@ const submitCreation = async () => {
payload.reference = reference
}
- if (creationForm.constructeurIds.length) {
- payload.constructeurIds = uniqueConstructeurIds(creationForm.constructeurIds)
- }
+ payload.constructeurIds = uniqueConstructeurIds(creationForm.constructeurIds)
const selectedProductId =
typeof creationForm.productId === 'string'
diff --git a/app/pages/product-catalog.vue b/app/pages/product-catalog.vue
index b216fb7..648f663 100644
--- a/app/pages/product-catalog.vue
+++ b/app/pages/product-catalog.vue
@@ -101,28 +101,44 @@
|
-
+
|
|
- {{ product.name }} |
- {{ product.reference || '—' }} |
- {{ product.typeProduct?.name || '—' }} |
+ {{ row.product.name }} |
+ {{ row.product.reference || '—' }} |
+ {{ row.product.typeProduct?.name || '—' }} |
-
- {{ formatConstructeurs(product.constructeurs) }}
-
+
+
+ {{ supplier }}
+
+
+ +{{ row.suppliers.overflow }}
+
+
—
|
- {{ formatPrice(product.supplierPrice) }}
+ {{ formatPrice(row.product.supplierPrice) }}
|
Modifier
@@ -130,7 +146,7 @@
@@ -233,11 +249,91 @@ const formatPrice = (value: any) => {
return priceFormatter.format(number)
}
-const formatConstructeurs = (constructeurs: Array>) =>
- constructeurs
- .map((constructeur) => constructeur?.name)
- .filter((name): name is string => Boolean(name))
- .join(', ')
+const MAX_VISIBLE_SUPPLIERS = 3
+
+const resolveProductSuppliers = (product: Record) => {
+ const names: string[] = []
+ const seen = new Set()
+
+ const pushName = (maybeName: unknown) => {
+ if (typeof maybeName !== 'string') {
+ return
+ }
+ const normalized = maybeName.trim().replace(/\s+/g, ' ')
+ if (!normalized.length) {
+ return
+ }
+ const key = normalized.toLowerCase()
+ if (seen.has(key)) {
+ return
+ }
+ seen.add(key)
+ names.push(normalized)
+ }
+
+ const collectConstructeurs = (value: unknown): void => {
+ if (!value) {
+ return
+ }
+ if (Array.isArray(value)) {
+ value.forEach(collectConstructeurs)
+ return
+ }
+ if (typeof value === 'string') {
+ pushName(value)
+ return
+ }
+ if (typeof value === 'object') {
+ const record = value as Record
+ pushName(record?.name ?? record?.label ?? record?.companyName ?? record?.company ?? null)
+ if (record?.constructeur) {
+ collectConstructeurs(record.constructeur)
+ }
+ if (Array.isArray(record?.constructeurs)) {
+ collectConstructeurs(record.constructeurs)
+ }
+ }
+ }
+
+ const collectFromLabel = (value: unknown): void => {
+ if (typeof value !== 'string') {
+ return
+ }
+ value
+ .split(/[,;\\/•·|]+/)
+ .map((part) => part.trim())
+ .filter(Boolean)
+ .forEach(pushName)
+ }
+
+ collectConstructeurs(product?.constructeurs)
+ collectConstructeurs(product?.constructeur)
+
+ collectFromLabel(product?.constructeursLabel)
+ collectFromLabel(product?.supplierLabel)
+ collectFromLabel(product?.suppliers)
+
+ return names
+}
+
+const buildSuppliersDisplay = (product: Record) => {
+ const suppliers = resolveProductSuppliers(product)
+ const visible = suppliers.slice(0, MAX_VISIBLE_SUPPLIERS)
+ const overflow = Math.max(suppliers.length - visible.length, 0)
+ return {
+ suppliers,
+ visible,
+ overflow,
+ tooltip: suppliers.length ? suppliers.join(', ') : '',
+ }
+}
+
+const productRows = computed(() =>
+ filteredProducts.value.map((product) => ({
+ product,
+ suppliers: buildSuppliersDisplay(product),
+ })),
+)
const resolvePrimaryDocument = (product: Record) => {
const documents = Array.isArray(product?.documents) ? product.documents : []
diff --git a/app/pages/product/[id]/edit.vue b/app/pages/product/[id]/edit.vue
index 5282a83..0fda70e 100644
--- a/app/pages/product/[id]/edit.vue
+++ b/app/pages/product/[id]/edit.vue
@@ -92,6 +92,7 @@
class="w-full"
:disabled="saving"
placeholder="Rechercher un ou plusieurs fournisseurs..."
+ :initial-options="product?.constructeurs || []"
/>
@@ -327,6 +328,7 @@ import { useProducts } from '~/composables/useProducts'
import { useCustomFields } from '~/composables/useCustomFields'
import { useToast } from '~/composables/useToast'
import { useDocuments } from '~/composables/useDocuments'
+import { useConstructeurs } from '~/composables/useConstructeurs'
import { formatProductStructurePreview, normalizeProductStructureForSave } from '~/shared/modelUtils'
import { uniqueConstructeurIds } from '~/shared/constructeurUtils'
import { getModelType } from '~/services/modelTypes'
@@ -356,6 +358,7 @@ const {
uploadDocuments: uploadProductDocuments,
deleteDocument: deleteProductDocument,
} = useDocuments()
+const { ensureConstructeurs } = useConstructeurs()
const product = ref(null)
const productType = ref(null)
@@ -490,7 +493,7 @@ const loadProduct = async () => {
product.value = result.data
productDocuments.value = Array.isArray(result.data?.documents) ? result.data.documents : []
await loadProductType()
- hydrateForm()
+ await hydrateForm()
await refreshDocuments()
} else {
product.value = null
@@ -566,7 +569,7 @@ const loadProductType = async () => {
}
}
-const hydrateForm = () => {
+const hydrateForm = async () => {
if (!product.value) {
return
}
@@ -580,6 +583,9 @@ const hydrateForm = () => {
? String(product.value.supplierPrice)
: ''
customFieldInputs.value = buildCustomFieldInputs(structure.value, product.value.customFieldValues)
+ if (editionForm.constructeurIds.length) {
+ await ensureConstructeurs(editionForm.constructeurIds)
+ }
}
watch(
@@ -677,10 +683,12 @@ const submitEdition = async () => {
return
}
+ const constructeurIds = uniqueConstructeurIds(editionForm.constructeurIds)
+
const payload: Record = {
name: editionForm.name.trim(),
reference: editionForm.reference.trim() || null,
- constructeurIds: uniqueConstructeurIds(editionForm.constructeurIds),
+ constructeurIds,
}
const rawPrice = editionForm.supplierPrice.trim()
diff --git a/app/pages/product/create.vue b/app/pages/product/create.vue
index a030fb4..791dda5 100644
--- a/app/pages/product/create.vue
+++ b/app/pages/product/create.vue
@@ -423,9 +423,7 @@ const buildPayload = () => {
payload.reference = reference
}
- if (creationForm.constructeurIds.length) {
- payload.constructeurIds = uniqueConstructeurIds(creationForm.constructeurIds)
- }
+ payload.constructeurIds = uniqueConstructeurIds(creationForm.constructeurIds)
const rawPrice = creationForm.supplierPrice.trim()
if (rawPrice) {
|