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:
@@ -42,6 +42,12 @@
|
||||
</span>
|
||||
</template>
|
||||
<span v-if="component.prix" class="badge badge-primary badge-sm">{{ component.prix }}€</span>
|
||||
<span
|
||||
v-if="displayProductName"
|
||||
class="badge badge-info badge-sm"
|
||||
>
|
||||
Produit : {{ displayProductName }}
|
||||
</span>
|
||||
<span
|
||||
v-if="component.typeMachineComponentRequirement"
|
||||
class="badge badge-outline badge-sm"
|
||||
@@ -124,6 +130,94 @@
|
||||
<span v-else class="font-medium">Non défini</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-control md:col-span-2">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Produit catalogue</span>
|
||||
</label>
|
||||
<div class="input input-bordered input-sm bg-base-200 min-h-[2.75rem] flex flex-col justify-center space-y-1">
|
||||
<template v-if="displayProduct">
|
||||
<span class="font-semibold text-base-content">
|
||||
{{ displayProductName || 'Produit catalogue' }}
|
||||
</span>
|
||||
<span
|
||||
v-for="info in productInfoRows"
|
||||
:key="info.label"
|
||||
class="text-xs text-base-content/70"
|
||||
>
|
||||
{{ info.label }} : {{ info.value }}
|
||||
</span>
|
||||
<NuxtLink
|
||||
v-if="component.product?.id"
|
||||
:to="`/product/${component.product.id}/edit`"
|
||||
class="link link-primary text-xs"
|
||||
>
|
||||
Ouvrir la fiche produit
|
||||
</NuxtLink>
|
||||
</template>
|
||||
<span v-else class="font-medium">Non défini</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="productDocuments.length"
|
||||
class="mt-2 space-y-2 rounded-md border border-base-200 bg-base-100 p-3 text-xs"
|
||||
>
|
||||
<h4 class="font-medium text-base-content">
|
||||
Documents du produit
|
||||
</h4>
|
||||
<div
|
||||
v-for="document in productDocuments"
|
||||
:key="document.id || document.path || document.name"
|
||||
class="flex items-center justify-between gap-3 rounded border border-base-200 bg-base-100 px-3 py-2"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="flex-shrink-0 overflow-hidden rounded-md border border-base-200 bg-base-200/70 flex items-center justify-center h-12 w-10"
|
||||
>
|
||||
<img
|
||||
v-if="isImageDocument(document) && document.path"
|
||||
:src="document.path"
|
||||
class="h-full w-full object-cover"
|
||||
:alt="`Aperçu de ${document.name}`"
|
||||
>
|
||||
<iframe
|
||||
v-else-if="shouldInlinePdf(document)"
|
||||
:src="documentPreviewSrc(document)"
|
||||
class="h-full w-full border-0 bg-white"
|
||||
title="Aperçu PDF"
|
||||
/>
|
||||
<component
|
||||
v-else
|
||||
:is="documentIcon(document).component"
|
||||
class="h-6 w-6"
|
||||
:class="documentIcon(document).colorClass"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-medium text-base-content">
|
||||
{{ document.name }}
|
||||
</div>
|
||||
<div class="text-xs text-base-content/70">
|
||||
{{ document.mimeType || 'Inconnu' }} • {{ formatSize(document.size) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost btn-xs"
|
||||
:disabled="!canPreviewDocument(document)"
|
||||
:title="canPreviewDocument(document) ? 'Consulter le document' : 'Aucun aperçu disponible pour ce type'"
|
||||
@click="openPreview(document)"
|
||||
>
|
||||
Consulter
|
||||
</button>
|
||||
<button type="button" class="btn btn-ghost btn-xs" @click="downloadDocument(document)">
|
||||
Télécharger
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -432,6 +526,110 @@ const childComponents = computed(() => {
|
||||
|
||||
const { constructeurs } = useConstructeurs()
|
||||
|
||||
const buildProductDisplay = (product) => {
|
||||
if (!product || typeof product !== 'object') {
|
||||
return null
|
||||
}
|
||||
|
||||
const suppliers = Array.isArray(product.constructeurs)
|
||||
? product.constructeurs
|
||||
.map((constructeur) => constructeur?.name)
|
||||
.filter((name) => typeof name === 'string' && name.trim().length > 0)
|
||||
.join(', ')
|
||||
: product.supplierLabel || null
|
||||
|
||||
const priceValue =
|
||||
product.supplierPrice ??
|
||||
product.price ??
|
||||
product.priceLabel ??
|
||||
product.priceDisplay ??
|
||||
null
|
||||
|
||||
let price = null
|
||||
if (priceValue !== null && priceValue !== undefined) {
|
||||
const parsed = Number(priceValue)
|
||||
if (!Number.isNaN(parsed)) {
|
||||
price = currencyFormatter.format(parsed)
|
||||
} else if (typeof priceValue === 'string' && priceValue.trim().length > 0) {
|
||||
price = priceValue
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name:
|
||||
product.name ||
|
||||
product.label ||
|
||||
product.reference ||
|
||||
product.productName ||
|
||||
null,
|
||||
reference: product.reference || null,
|
||||
category: product.typeProduct?.name || product.category || null,
|
||||
suppliers,
|
||||
price,
|
||||
}
|
||||
}
|
||||
|
||||
const displayProduct = computed(() => {
|
||||
const explicit = props.component.product || null
|
||||
const normalized = buildProductDisplay(explicit)
|
||||
if (normalized) {
|
||||
return normalized
|
||||
}
|
||||
const fallback = props.component.__productDisplay
|
||||
if (fallback) {
|
||||
return {
|
||||
name: fallback.name || null,
|
||||
reference: fallback.reference || null,
|
||||
category: fallback.category || null,
|
||||
suppliers: fallback.suppliers || null,
|
||||
price: fallback.price || null,
|
||||
}
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
const displayProductName = computed(() => {
|
||||
if (displayProduct.value?.name) {
|
||||
return displayProduct.value.name
|
||||
}
|
||||
return (
|
||||
props.component.product?.name ||
|
||||
props.component.productName ||
|
||||
props.component.productLabel ||
|
||||
null
|
||||
)
|
||||
})
|
||||
|
||||
const displayProductCategory = computed(() => displayProduct.value?.category || null)
|
||||
const displayProductReference = computed(() => displayProduct.value?.reference || null)
|
||||
const displayProductSuppliers = computed(() => displayProduct.value?.suppliers || null)
|
||||
const displayProductPrice = computed(() => displayProduct.value?.price || null)
|
||||
|
||||
const productInfoRows = computed(() => {
|
||||
if (!displayProduct.value) {
|
||||
return []
|
||||
}
|
||||
const rows = []
|
||||
if (displayProductReference.value) {
|
||||
rows.push({ label: 'Référence', value: displayProductReference.value })
|
||||
}
|
||||
if (displayProductPrice.value) {
|
||||
rows.push({ label: 'Prix indicatif', value: displayProductPrice.value })
|
||||
}
|
||||
if (displayProductSuppliers.value) {
|
||||
rows.push({ label: 'Fournisseur(s)', value: displayProductSuppliers.value })
|
||||
}
|
||||
if (displayProductCategory.value) {
|
||||
rows.push({ label: 'Catégorie', value: displayProductCategory.value })
|
||||
}
|
||||
return rows
|
||||
})
|
||||
|
||||
const productDocuments = computed(() => {
|
||||
const product = props.component.product
|
||||
return Array.isArray(product?.documents) ? product.documents : []
|
||||
})
|
||||
|
||||
const componentConstructeurIds = computed(() =>
|
||||
uniqueConstructeurIds(
|
||||
props.component,
|
||||
|
||||
Reference in New Issue
Block a user