From 936a73fde3bf4c92ebec0abfd82e0ee7203c11a2 Mon Sep 17 00:00:00 2001 From: Matthieu Date: Wed, 3 Dec 2025 11:29:11 +0100 Subject: [PATCH] Fix fournisseur handling across catalog flows --- app/components/ComponentItem.vue | 1 + app/components/ConstructeurSelect.vue | 41 +++++++- app/components/PieceItem.vue | 1 + app/composables/useApi.js | 21 ++++- app/composables/useComposants.js | 35 ++++++- app/composables/useConstructeurs.js | 104 +++++++++++++++++++-- app/composables/usePieces.js | 35 ++++++- app/composables/useProducts.js | 49 ++++++++-- app/pages/component/[id]/edit.vue | 6 ++ app/pages/machine/[id].vue | 1 + app/pages/pieces-catalog.vue | 128 +++++++++++++++++++++++-- app/pages/pieces/[id]/edit.vue | 10 +- app/pages/pieces/create.vue | 4 +- app/pages/product-catalog.vue | 130 ++++++++++++++++++++++---- app/pages/product/[id]/edit.vue | 14 ++- app/pages/product/create.vue | 4 +- 16 files changed, 519 insertions(+), 65 deletions(-) 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) {