Fix fournisseur handling across catalog flows

This commit is contained in:
Matthieu
2025-12-03 11:29:11 +01:00
parent 34af59d054
commit 936a73fde3
16 changed files with 519 additions and 65 deletions

View File

@@ -102,6 +102,7 @@
class="w-full"
:disabled="saving"
placeholder="Rechercher un ou plusieurs fournisseurs..."
:initial-options="component?.constructeurs || []"
/>
</div>
</div>
@@ -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,

View File

@@ -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"
/>

View File

@@ -93,25 +93,48 @@
<th class="w-24">Aperçu</th>
<th>Nom</th>
<th>Référence</th>
<th>Fournisseurs</th>
<th>Type de pièce</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="piece in visiblePieces" :key="piece.id">
<tr v-for="row in pieceRows" :key="row.piece.id">
<td class="align-middle">
<DocumentThumbnail
:document="resolvePrimaryDocument(piece)"
:alt="resolvePreviewAlt(piece)"
:document="resolvePrimaryDocument(row.piece)"
:alt="resolvePreviewAlt(row.piece)"
/>
</td>
<td>{{ piece.name || 'Pièce sans nom' }}</td>
<td>{{ piece.reference || '—' }}</td>
<td>{{ resolvePieceType(piece) }}</td>
<td>{{ row.piece.name || 'Pièce sans nom' }}</td>
<td>{{ row.piece.reference || '—' }}</td>
<td>
<div
v-if="row.suppliers.visible.length"
class="flex max-w-[14rem] flex-wrap items-center gap-1"
:title="row.suppliers.tooltip"
>
<span
v-for="supplier in row.suppliers.visible"
:key="supplier"
class="badge badge-ghost badge-sm whitespace-nowrap"
>
{{ supplier }}
</span>
<span
v-if="row.suppliers.overflow"
class="badge badge-outline badge-sm"
>
+{{ row.suppliers.overflow }}
</span>
</div>
<span v-else></span>
</td>
<td>{{ resolvePieceType(row.piece) }}</td>
<td>
<div class="flex items-center gap-2">
<NuxtLink
:to="`/pieces/${piece.id}/edit`"
:to="`/pieces/${row.piece.id}/edit`"
class="btn btn-ghost btn-xs"
>
Modifier
@@ -120,7 +143,7 @@
type="button"
class="btn btn-error btn-xs"
:disabled="loadingPieces"
@click="handleDeletePiece(piece)"
@click="handleDeletePiece(row.piece)"
>
Supprimer
</button>
@@ -193,6 +216,88 @@ const resolvePieceType = (piece: Record<string, any>) => {
return '—'
}
const MAX_VISIBLE_SUPPLIERS = 3
const resolvePieceSuppliers = (piece: Record<string, any>) => {
const names: string[] = []
const seen = new Set<string>()
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<string, any>
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<string, any>) => {
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<string, any>) => {
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<string, any>) => {
const { blockingReasons, hasCustomFields } = resolveDeleteGuard(piece)

View File

@@ -102,6 +102,7 @@
class="w-full"
:disabled="saving"
placeholder="Rechercher un ou plusieurs fournisseurs..."
:initial-options="piece?.constructeurs || []"
/>
</div>
</div>
@@ -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<any | null>(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<string, any> = {
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'

View File

@@ -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'

View File

@@ -101,28 +101,44 @@
</tr>
</thead>
<tbody>
<tr v-for="product in filteredProducts" :key="product.id">
<tr v-for="row in productRows" :key="row.product.id">
<td class="align-middle">
<DocumentThumbnail
:document="resolvePrimaryDocument(product)"
:alt="resolvePreviewAlt(product)"
:document="resolvePrimaryDocument(row.product)"
:alt="resolvePreviewAlt(row.product)"
/>
</td>
<td class="font-medium">{{ product.name }}</td>
<td>{{ product.reference || '—' }}</td>
<td>{{ product.typeProduct?.name || '—' }}</td>
<td class="font-medium">{{ row.product.name }}</td>
<td>{{ row.product.reference || '—' }}</td>
<td>{{ row.product.typeProduct?.name || '—' }}</td>
<td>
<span v-if="product.constructeurs?.length" class="text-sm">
{{ formatConstructeurs(product.constructeurs) }}
</span>
<div
v-if="row.suppliers.visible.length"
class="flex max-w-[14rem] flex-wrap items-center gap-1 text-sm"
:title="row.suppliers.tooltip"
>
<span
v-for="supplier in row.suppliers.visible"
:key="supplier"
class="badge badge-ghost badge-sm whitespace-nowrap"
>
{{ supplier }}
</span>
<span
v-if="row.suppliers.overflow"
class="badge badge-outline badge-sm"
>
+{{ row.suppliers.overflow }}
</span>
</div>
<span v-else class="text-sm text-base-content/50"></span>
</td>
<td class="text-right">
{{ formatPrice(product.supplierPrice) }}
{{ formatPrice(row.product.supplierPrice) }}
</td>
<td class="text-right space-x-2">
<NuxtLink
:to="`/product/${product.id}/edit`"
:to="`/product/${row.product.id}/edit`"
class="btn btn-ghost btn-xs"
>
Modifier
@@ -130,7 +146,7 @@
<button
type="button"
class="btn btn-ghost btn-xs text-error"
@click="confirmDelete(product)"
@click="confirmDelete(row.product)"
>
Supprimer
</button>
@@ -233,11 +249,91 @@ const formatPrice = (value: any) => {
return priceFormatter.format(number)
}
const formatConstructeurs = (constructeurs: Array<Record<string, any>>) =>
constructeurs
.map((constructeur) => constructeur?.name)
.filter((name): name is string => Boolean(name))
.join(', ')
const MAX_VISIBLE_SUPPLIERS = 3
const resolveProductSuppliers = (product: Record<string, any>) => {
const names: string[] = []
const seen = new Set<string>()
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<string, any>
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<string, any>) => {
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<string, any>) => {
const documents = Array.isArray(product?.documents) ? product.documents : []

View File

@@ -92,6 +92,7 @@
class="w-full"
:disabled="saving"
placeholder="Rechercher un ou plusieurs fournisseurs..."
:initial-options="product?.constructeurs || []"
/>
</div>
</div>
@@ -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<any | null>(null)
const productType = ref<any | null>(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<string, any> = {
name: editionForm.name.trim(),
reference: editionForm.reference.trim() || null,
constructeurIds: uniqueConstructeurIds(editionForm.constructeurIds),
constructeurIds,
}
const rawPrice = editionForm.supplierPrice.trim()

View File

@@ -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) {