refactor(frontend) : extract DocumentListInline shared component
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -309,74 +309,14 @@
|
|||||||
@files-added="handleFilesAdded"
|
@files-added="handleFilesAdded"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div v-if="componentDocuments.length" class="space-y-2">
|
<DocumentListInline
|
||||||
<div
|
:documents="componentDocuments"
|
||||||
v-for="document in componentDocuments"
|
:can-delete="isEditMode"
|
||||||
:key="document.id"
|
:delete-disabled="uploadingDocuments"
|
||||||
class="flex items-center justify-between rounded border border-base-200 bg-base-100 px-3 py-2"
|
empty-text="Aucun document lié à ce composant."
|
||||||
>
|
@preview="openPreview"
|
||||||
<div class="flex items-center gap-3 text-sm">
|
@delete="removeDocument"
|
||||||
<div
|
/>
|
||||||
class="flex-shrink-0 overflow-hidden rounded-md border border-base-200 bg-base-200/70 flex items-center justify-center"
|
|
||||||
:class="documentThumbnailClass(document)"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
v-if="isImageDocument(document) && (document.fileUrl || document.path)"
|
|
||||||
:src="document.fileUrl || 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">
|
|
||||||
{{ document.name }}
|
|
||||||
</div>
|
|
||||||
<div class="text-xs text-gray-500">
|
|
||||||
{{ document.mimeType || 'Inconnu' }} • {{ formatSize(document.size) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<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>
|
|
||||||
<button
|
|
||||||
v-if="isEditMode"
|
|
||||||
type="button"
|
|
||||||
class="btn btn-error btn-xs"
|
|
||||||
:disabled="uploadingDocuments"
|
|
||||||
@click="removeDocument(document.id)"
|
|
||||||
>
|
|
||||||
Supprimer
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p v-else-if="!loadingDocuments" class="text-xs text-gray-500">
|
|
||||||
Aucun document lié à ce composant.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Component Pieces -->
|
<!-- Component Pieces -->
|
||||||
@@ -438,7 +378,6 @@ import {
|
|||||||
formatSize,
|
formatSize,
|
||||||
shouldInlinePdf,
|
shouldInlinePdf,
|
||||||
documentPreviewSrc,
|
documentPreviewSrc,
|
||||||
documentThumbnailClass,
|
|
||||||
documentIcon,
|
documentIcon,
|
||||||
downloadDocument,
|
downloadDocument,
|
||||||
} from '~/shared/utils/documentDisplayUtils'
|
} from '~/shared/utils/documentDisplayUtils'
|
||||||
|
|||||||
@@ -398,83 +398,14 @@
|
|||||||
@files-added="handleFilesAdded"
|
@files-added="handleFilesAdded"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div v-if="pieceDocuments.length" class="space-y-2">
|
<DocumentListInline
|
||||||
<div
|
:documents="pieceDocuments"
|
||||||
v-for="document in pieceDocuments"
|
:can-delete="isEditMode"
|
||||||
:key="document.id"
|
:delete-disabled="uploadingDocuments"
|
||||||
class="flex items-center justify-between rounded border border-base-200 bg-base-100 px-3 py-2"
|
empty-text="Aucun document lié à cette pièce."
|
||||||
>
|
@preview="openPreview"
|
||||||
<div class="flex items-center gap-3 text-sm">
|
@delete="removeDocument"
|
||||||
<div
|
/>
|
||||||
class="flex-shrink-0 overflow-hidden rounded-md border border-base-200 bg-base-200/70 flex items-center justify-center"
|
|
||||||
:class="documentThumbnailClass(document)"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
v-if="isImageDocument(document) && (document.fileUrl || document.path)"
|
|
||||||
:src="document.fileUrl || 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">
|
|
||||||
{{ document.name }}
|
|
||||||
</div>
|
|
||||||
<div class="text-xs text-gray-500">
|
|
||||||
{{ document.mimeType || "Inconnu" }} •
|
|
||||||
{{ formatSize(document.size) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<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>
|
|
||||||
<button
|
|
||||||
v-if="isEditMode"
|
|
||||||
type="button"
|
|
||||||
class="btn btn-error btn-xs"
|
|
||||||
:disabled="uploadingDocuments"
|
|
||||||
@click="removeDocument(document.id)"
|
|
||||||
>
|
|
||||||
Supprimer
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p v-else-if="!loadingDocuments" class="text-xs text-gray-500">
|
|
||||||
Aucun document lié à cette pièce.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -499,7 +430,6 @@ import {
|
|||||||
formatSize,
|
formatSize,
|
||||||
shouldInlinePdf,
|
shouldInlinePdf,
|
||||||
documentPreviewSrc,
|
documentPreviewSrc,
|
||||||
documentThumbnailClass,
|
|
||||||
documentIcon,
|
documentIcon,
|
||||||
downloadDocument,
|
downloadDocument,
|
||||||
} from '~/shared/utils/documentDisplayUtils'
|
} from '~/shared/utils/documentDisplayUtils'
|
||||||
|
|||||||
104
app/components/common/DocumentListInline.vue
Normal file
104
app/components/common/DocumentListInline.vue
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="documents.length" class="space-y-2">
|
||||||
|
<div
|
||||||
|
v-for="document in documents"
|
||||||
|
:key="document.id || document.path || document.name"
|
||||||
|
class="flex items-center justify-between rounded border border-base-200 bg-base-100 px-3 py-2"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-3 text-sm">
|
||||||
|
<div
|
||||||
|
class="flex-shrink-0 overflow-hidden rounded-md border border-base-200 bg-base-200/70 flex items-center justify-center"
|
||||||
|
:class="documentThumbnailClass(document)"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
v-if="isImageDocument(document) && (document.fileUrl || document.path)"
|
||||||
|
:src="document.fileUrl || 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">
|
||||||
|
{{ 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">
|
||||||
|
<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="$emit('preview', document)"
|
||||||
|
>
|
||||||
|
Consulter
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-ghost btn-xs"
|
||||||
|
@click="downloadDocument(document)"
|
||||||
|
>
|
||||||
|
Télécharger
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="canDelete"
|
||||||
|
type="button"
|
||||||
|
class="btn btn-error btn-xs"
|
||||||
|
:disabled="deleteDisabled"
|
||||||
|
@click="$emit('delete', document.id)"
|
||||||
|
>
|
||||||
|
Supprimer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p v-else class="text-xs text-base-content/70">
|
||||||
|
{{ emptyText }}
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { canPreviewDocument, isImageDocument } from '~/utils/documentPreview'
|
||||||
|
import {
|
||||||
|
documentIcon,
|
||||||
|
formatSize,
|
||||||
|
shouldInlinePdf,
|
||||||
|
documentPreviewSrc,
|
||||||
|
documentThumbnailClass,
|
||||||
|
downloadDocument,
|
||||||
|
} from '~/shared/utils/documentDisplayUtils'
|
||||||
|
|
||||||
|
import type { Document } from '~/composables/useDocuments'
|
||||||
|
|
||||||
|
withDefaults(defineProps<{
|
||||||
|
documents: Document[]
|
||||||
|
canDelete?: boolean
|
||||||
|
deleteDisabled?: boolean
|
||||||
|
emptyText?: string
|
||||||
|
}>(), {
|
||||||
|
canDelete: false,
|
||||||
|
deleteDisabled: false,
|
||||||
|
emptyText: 'Aucun document.',
|
||||||
|
})
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
(e: 'preview', document: Document): void
|
||||||
|
(e: 'delete', documentId: string): void
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
@@ -304,78 +304,15 @@
|
|||||||
<p v-else-if="loadingDocuments" class="text-xs text-base-content/70">
|
<p v-else-if="loadingDocuments" class="text-xs text-base-content/70">
|
||||||
Chargement des documents en cours…
|
Chargement des documents en cours…
|
||||||
</p>
|
</p>
|
||||||
<div v-else-if="componentDocuments.length" class="space-y-2">
|
<DocumentListInline
|
||||||
<div
|
v-else
|
||||||
v-for="document in componentDocuments"
|
:documents="componentDocuments"
|
||||||
:key="document.id || document.path || document.name"
|
:can-delete="canEdit"
|
||||||
class="flex items-center justify-between rounded border border-base-200 bg-base-100 px-3 py-2"
|
:delete-disabled="uploadingDocuments"
|
||||||
>
|
empty-text="Aucun document n'est associé à ce composant pour le moment."
|
||||||
<div class="flex items-center gap-3 text-sm">
|
@preview="openPreview"
|
||||||
<div
|
@delete="removeDocument"
|
||||||
class="flex-shrink-0 overflow-hidden rounded-md border border-base-200 bg-base-200/70 flex items-center justify-center"
|
/>
|
||||||
:class="documentThumbnailClass(document)"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
v-if="isImageDocument(document) && (document.fileUrl || document.path)"
|
|
||||||
:src="document.fileUrl || 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">
|
|
||||||
{{ 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">
|
|
||||||
<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>
|
|
||||||
<button
|
|
||||||
v-if="canEdit"
|
|
||||||
type="button"
|
|
||||||
class="btn btn-error btn-xs"
|
|
||||||
:disabled="uploadingDocuments"
|
|
||||||
@click="removeDocument(document.id)"
|
|
||||||
>
|
|
||||||
Supprimer
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p v-else class="text-xs text-base-content/70">
|
|
||||||
Aucun document n'est associé à ce composant pour le moment.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<EntityHistorySection
|
<EntityHistorySection
|
||||||
@@ -430,6 +367,17 @@ import { useConstructeurs } from '~/composables/useConstructeurs'
|
|||||||
import { useComponentHistory } from '~/composables/useComponentHistory'
|
import { useComponentHistory } from '~/composables/useComponentHistory'
|
||||||
import { formatStructurePreview, normalizeStructureForEditor } from '~/shared/modelUtils'
|
import { formatStructurePreview, normalizeStructureForEditor } from '~/shared/modelUtils'
|
||||||
import { uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
import { uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
||||||
|
import {
|
||||||
|
getStructureCustomFields,
|
||||||
|
getStructurePieces,
|
||||||
|
getStructureProducts,
|
||||||
|
getStructureSubcomponents,
|
||||||
|
resolvePieceLabel as _resolvePieceLabel,
|
||||||
|
resolveProductLabel as _resolveProductLabel,
|
||||||
|
resolveSubcomponentLabel,
|
||||||
|
fetchModelTypeNames,
|
||||||
|
buildTypeLabelMap,
|
||||||
|
} from '~/shared/utils/structureDisplayUtils'
|
||||||
import type { ComponentModelStructure } from '~/shared/types/inventory'
|
import type { ComponentModelStructure } from '~/shared/types/inventory'
|
||||||
import type { ModelType } from '~/services/modelTypes'
|
import type { ModelType } from '~/services/modelTypes'
|
||||||
import { canPreviewDocument } from '~/utils/documentPreview'
|
import { canPreviewDocument } from '~/utils/documentPreview'
|
||||||
@@ -439,14 +387,6 @@ import {
|
|||||||
requiredCustomFieldsFilled as _requiredCustomFieldsFilled,
|
requiredCustomFieldsFilled as _requiredCustomFieldsFilled,
|
||||||
saveCustomFieldValues as _saveCustomFieldValues,
|
saveCustomFieldValues as _saveCustomFieldValues,
|
||||||
} from '~/shared/utils/customFieldFormUtils'
|
} from '~/shared/utils/customFieldFormUtils'
|
||||||
import {
|
|
||||||
documentIcon,
|
|
||||||
formatSize,
|
|
||||||
shouldInlinePdf,
|
|
||||||
documentPreviewSrc,
|
|
||||||
documentThumbnailClass,
|
|
||||||
downloadDocument,
|
|
||||||
} from '~/shared/utils/documentDisplayUtils'
|
|
||||||
|
|
||||||
interface ComponentCatalogType extends ModelType {
|
interface ComponentCatalogType extends ModelType {
|
||||||
structure: ComponentModelStructure | null
|
structure: ComponentModelStructure | null
|
||||||
@@ -505,23 +445,13 @@ const editionForm = reactive({
|
|||||||
|
|
||||||
const customFieldInputs = ref<CustomFieldInput[]>([])
|
const customFieldInputs = ref<CustomFieldInput[]>([])
|
||||||
const fetchedPieceTypeMap = ref<Record<string, string>>({})
|
const fetchedPieceTypeMap = ref<Record<string, string>>({})
|
||||||
const pieceTypeLabelMap = computed(() => ({
|
const pieceTypeLabelMap = computed(() =>
|
||||||
...Object.fromEntries(
|
buildTypeLabelMap(pieceTypes.value, fetchedPieceTypeMap.value),
|
||||||
(pieceTypes.value || [])
|
)
|
||||||
.filter((type: any) => type?.id)
|
|
||||||
.map((type: any) => [type.id, type.name || type.code || '']),
|
|
||||||
),
|
|
||||||
...fetchedPieceTypeMap.value,
|
|
||||||
}))
|
|
||||||
const fetchedProductTypeMap = ref<Record<string, string>>({})
|
const fetchedProductTypeMap = ref<Record<string, string>>({})
|
||||||
const productTypeLabelMap = computed(() => ({
|
const productTypeLabelMap = computed(() =>
|
||||||
...Object.fromEntries(
|
buildTypeLabelMap(productTypes.value, fetchedProductTypeMap.value),
|
||||||
(productTypes.value || [])
|
)
|
||||||
.filter((type: any) => type?.id)
|
|
||||||
.map((type: any) => [type.id, type.name || type.code || '']),
|
|
||||||
),
|
|
||||||
...fetchedProductTypeMap.value,
|
|
||||||
}))
|
|
||||||
const pieceCatalogMap = computed(() =>
|
const pieceCatalogMap = computed(() =>
|
||||||
new Map(
|
new Map(
|
||||||
(pieces.value || [])
|
(pieces.value || [])
|
||||||
@@ -660,52 +590,45 @@ const fetchComponent = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let initialized = false
|
const initialized = ref(false)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
[component, selectedTypeStructure],
|
[component, selectedTypeStructure],
|
||||||
([currentComponent, currentStructure]) => {
|
([currentComponent, currentStructure]) => {
|
||||||
if (!currentComponent || initialized) {
|
if (!currentComponent) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolvedTypeId = currentComponent.typeComposantId
|
if (!initialized.value) {
|
||||||
|| extractRelationId(currentComponent.typeComposant)
|
const resolvedTypeId = currentComponent.typeComposantId
|
||||||
|| ''
|
|| extractRelationId(currentComponent.typeComposant)
|
||||||
if (resolvedTypeId && !currentComponent.typeComposantId) {
|
|| ''
|
||||||
currentComponent.typeComposantId = resolvedTypeId
|
if (resolvedTypeId && !currentComponent.typeComposantId) {
|
||||||
}
|
currentComponent.typeComposantId = resolvedTypeId
|
||||||
selectedTypeId.value = resolvedTypeId
|
}
|
||||||
|
selectedTypeId.value = resolvedTypeId
|
||||||
|
|
||||||
editionForm.name = currentComponent.name || ''
|
editionForm.name = currentComponent.name || ''
|
||||||
editionForm.description = currentComponent.description || ''
|
editionForm.description = currentComponent.description || ''
|
||||||
editionForm.reference = currentComponent.reference || ''
|
editionForm.reference = currentComponent.reference || ''
|
||||||
editionForm.constructeurIds = uniqueConstructeurIds(
|
editionForm.constructeurIds = uniqueConstructeurIds(
|
||||||
currentComponent,
|
currentComponent,
|
||||||
Array.isArray(currentComponent.constructeurs) ? currentComponent.constructeurs : [],
|
Array.isArray(currentComponent.constructeurs) ? currentComponent.constructeurs : [],
|
||||||
currentComponent.constructeur ? [currentComponent.constructeur] : [],
|
currentComponent.constructeur ? [currentComponent.constructeur] : [],
|
||||||
)
|
)
|
||||||
editionForm.prix = currentComponent.prix !== null && currentComponent.prix !== undefined ? String(currentComponent.prix) : ''
|
editionForm.prix = currentComponent.prix !== null && currentComponent.prix !== undefined ? String(currentComponent.prix) : ''
|
||||||
if (editionForm.constructeurIds.length) {
|
if (editionForm.constructeurIds.length) {
|
||||||
void ensureConstructeurs(editionForm.constructeurIds)
|
void ensureConstructeurs(editionForm.constructeurIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
initialized.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// After setting selectedTypeId, read selectedTypeStructure.value (now updated) instead of
|
|
||||||
// the stale destructured currentStructure which was captured before the ID change.
|
|
||||||
refreshCustomFieldInputs(selectedTypeStructure.value ?? currentStructure, currentComponent.customFieldValues)
|
refreshCustomFieldInputs(selectedTypeStructure.value ?? currentStructure, currentComponent.customFieldValues)
|
||||||
|
|
||||||
initialized = true
|
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
watch(selectedTypeStructure, (currentStructure) => {
|
|
||||||
if (!component.value) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
refreshCustomFieldInputs(currentStructure, component.value.customFieldValues)
|
|
||||||
})
|
|
||||||
|
|
||||||
const submitEdition = async () => {
|
const submitEdition = async () => {
|
||||||
if (!component.value) {
|
if (!component.value) {
|
||||||
return
|
return
|
||||||
@@ -757,116 +680,14 @@ const submitEdition = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getStructureCustomFields = (structure: ComponentModelStructure | null) => {
|
|
||||||
return Array.isArray(structure?.customFields) ? structure.customFields : []
|
|
||||||
}
|
|
||||||
|
|
||||||
const getStructurePieces = (structure: ComponentModelStructure | null) => {
|
|
||||||
return Array.isArray(structure?.pieces) ? structure.pieces : []
|
|
||||||
}
|
|
||||||
|
|
||||||
const getStructureProducts = (structure: ComponentModelStructure | null) => {
|
|
||||||
return Array.isArray(structure?.products) ? structure.products : []
|
|
||||||
}
|
|
||||||
|
|
||||||
const getStructureSubcomponents = (structure: ComponentModelStructure | null) => {
|
|
||||||
if (Array.isArray(structure?.subcomponents)) {
|
|
||||||
return structure.subcomponents
|
|
||||||
}
|
|
||||||
const legacy = (structure as any)?.subComponents
|
|
||||||
return Array.isArray(legacy) ? legacy : []
|
|
||||||
}
|
|
||||||
|
|
||||||
const isNonEmptyString = (value: unknown): value is string =>
|
const isNonEmptyString = (value: unknown): value is string =>
|
||||||
typeof value === 'string' && value.trim().length > 0
|
typeof value === 'string' && value.trim().length > 0
|
||||||
|
|
||||||
const resolvePieceLabel = (piece: Record<string, any>) => {
|
const resolvePieceLabel = (piece: Record<string, any>) =>
|
||||||
const parts: string[] = []
|
_resolvePieceLabel(piece, pieceTypeLabelMap.value)
|
||||||
if (piece.role) {
|
|
||||||
parts.push(piece.role)
|
|
||||||
}
|
|
||||||
if (piece.typePiece?.name) {
|
|
||||||
parts.push(piece.typePiece.name)
|
|
||||||
} else if (piece.typePieceLabel) {
|
|
||||||
parts.push(piece.typePieceLabel)
|
|
||||||
} else if (piece.typePieceId && pieceTypeLabelMap.value[piece.typePieceId]) {
|
|
||||||
parts.push(pieceTypeLabelMap.value[piece.typePieceId])
|
|
||||||
} else if (piece.typePiece?.code) {
|
|
||||||
parts.push(`Famille ${piece.typePiece.code}`)
|
|
||||||
} else if (piece.familyCode) {
|
|
||||||
parts.push(`Famille ${piece.familyCode}`)
|
|
||||||
} else if (piece.typePieceId) {
|
|
||||||
parts.push(`#${piece.typePieceId}`)
|
|
||||||
}
|
|
||||||
return parts.length ? parts.join(' • ') : 'Pièce'
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchPieceTypeNames = async (ids: string[]) => {
|
const resolveProductLabel = (product: Record<string, any>) =>
|
||||||
const missing = ids.filter((id) => id && !pieceTypeLabelMap.value[id])
|
_resolveProductLabel(product, productTypeLabelMap.value)
|
||||||
if (!missing.length) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const results = await Promise.allSettled(
|
|
||||||
missing.map((id) => get(`/model_types/${id}`)),
|
|
||||||
)
|
|
||||||
const next = { ...fetchedPieceTypeMap.value }
|
|
||||||
results.forEach((result, index) => {
|
|
||||||
const key = missing[index]
|
|
||||||
if (!key || result.status !== 'fulfilled') {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const data = result.value?.data
|
|
||||||
const name = data?.name || data?.code
|
|
||||||
if (name) {
|
|
||||||
next[key] = name
|
|
||||||
}
|
|
||||||
})
|
|
||||||
fetchedPieceTypeMap.value = next
|
|
||||||
}
|
|
||||||
|
|
||||||
const resolveProductLabel = (product: Record<string, any>) => {
|
|
||||||
const parts: string[] = []
|
|
||||||
if (product.role) {
|
|
||||||
parts.push(product.role)
|
|
||||||
}
|
|
||||||
if (product.typeProduct?.name) {
|
|
||||||
parts.push(product.typeProduct.name)
|
|
||||||
} else if (product.typeProductLabel) {
|
|
||||||
parts.push(product.typeProductLabel)
|
|
||||||
} else if (product.typeProductId && productTypeLabelMap.value[product.typeProductId]) {
|
|
||||||
parts.push(productTypeLabelMap.value[product.typeProductId])
|
|
||||||
} else if (product.typeProduct?.code) {
|
|
||||||
parts.push(`Catégorie ${product.typeProduct.code}`)
|
|
||||||
} else if (product.familyCode) {
|
|
||||||
parts.push(`Catégorie ${product.familyCode}`)
|
|
||||||
} else if (product.typeProductId) {
|
|
||||||
parts.push(`#${product.typeProductId}`)
|
|
||||||
}
|
|
||||||
return parts.length ? parts.join(' • ') : 'Produit'
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchProductTypeNames = async (ids: string[]) => {
|
|
||||||
const missing = ids.filter((id) => id && !productTypeLabelMap.value[id])
|
|
||||||
if (!missing.length) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const results = await Promise.allSettled(
|
|
||||||
missing.map((id) => get(`/model_types/${id}`)),
|
|
||||||
)
|
|
||||||
const next = { ...fetchedProductTypeMap.value }
|
|
||||||
results.forEach((result, index) => {
|
|
||||||
const key = missing[index]
|
|
||||||
if (!key || result.status !== 'fulfilled') {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const data = result.value?.data
|
|
||||||
const name = data?.name || data?.code
|
|
||||||
if (name) {
|
|
||||||
next[key] = name
|
|
||||||
}
|
|
||||||
})
|
|
||||||
fetchedProductTypeMap.value = next
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
selectedTypeStructure,
|
selectedTypeStructure,
|
||||||
@@ -875,45 +696,31 @@ watch(
|
|||||||
.map((piece: any) => piece?.typePieceId)
|
.map((piece: any) => piece?.typePieceId)
|
||||||
.filter((id: any): id is string => typeof id === 'string' && id.trim().length > 0)
|
.filter((id: any): id is string => typeof id === 'string' && id.trim().length > 0)
|
||||||
if (pieceIds.length) {
|
if (pieceIds.length) {
|
||||||
fetchPieceTypeNames(Array.from(new Set(pieceIds))).catch(() => {})
|
fetchModelTypeNames(Array.from(new Set(pieceIds)), pieceTypeLabelMap.value, get)
|
||||||
|
.then((additions) => {
|
||||||
|
if (Object.keys(additions).length) {
|
||||||
|
fetchedPieceTypeMap.value = { ...fetchedPieceTypeMap.value, ...additions }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
}
|
}
|
||||||
|
|
||||||
const productIds = getStructureProducts(structure)
|
const productIds = getStructureProducts(structure)
|
||||||
.map((product: any) => product?.typeProductId)
|
.map((product: any) => product?.typeProductId)
|
||||||
.filter((id: any): id is string => typeof id === 'string' && id.trim().length > 0)
|
.filter((id: any): id is string => typeof id === 'string' && id.trim().length > 0)
|
||||||
if (productIds.length) {
|
if (productIds.length) {
|
||||||
fetchProductTypeNames(Array.from(new Set(productIds))).catch(() => {})
|
fetchModelTypeNames(Array.from(new Set(productIds)), productTypeLabelMap.value, get)
|
||||||
|
.then((additions) => {
|
||||||
|
if (Object.keys(additions).length) {
|
||||||
|
fetchedProductTypeMap.value = { ...fetchedProductTypeMap.value, ...additions }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
const resolveSubcomponentLabel = (node: Record<string, any>) => {
|
|
||||||
const parts: string[] = []
|
|
||||||
if (node.alias) {
|
|
||||||
parts.push(node.alias)
|
|
||||||
}
|
|
||||||
if (node.typeComposant?.name) {
|
|
||||||
parts.push(node.typeComposant.name)
|
|
||||||
} else if (node.typeComposantLabel) {
|
|
||||||
parts.push(node.typeComposantLabel)
|
|
||||||
} else if (node.familyCode) {
|
|
||||||
parts.push(node.familyCode)
|
|
||||||
} else if (node.typeComposantId) {
|
|
||||||
parts.push(`#${node.typeComposantId}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const childCount = Array.isArray(node.subcomponents)
|
|
||||||
? node.subcomponents.length
|
|
||||||
: Array.isArray(node.subComponents)
|
|
||||||
? node.subComponents.length
|
|
||||||
: 0
|
|
||||||
if (childCount) {
|
|
||||||
parts.push(`${childCount} sous-composant(s)`)
|
|
||||||
}
|
|
||||||
return parts.length ? parts.join(' • ') : 'Sous-composant'
|
|
||||||
}
|
|
||||||
|
|
||||||
type SelectionEntry = {
|
type SelectionEntry = {
|
||||||
id: string
|
id: string
|
||||||
path: string
|
path: string
|
||||||
@@ -1021,11 +828,13 @@ onMounted(async () => {
|
|||||||
])
|
])
|
||||||
loading.value = false
|
loading.value = false
|
||||||
|
|
||||||
// Defer bulk catalog loads — not needed for initial render
|
// Defer bulk catalog loads — only needed when component has structure selections
|
||||||
Promise.allSettled([
|
if (component.value?.structure) {
|
||||||
loadPieces({ itemsPerPage: 200 }),
|
Promise.allSettled([
|
||||||
loadProducts({ itemsPerPage: 200 }),
|
loadPieces({ itemsPerPage: 200 }),
|
||||||
loadComposants({ itemsPerPage: 200 }),
|
loadProducts({ itemsPerPage: 200 }),
|
||||||
]).catch(() => {})
|
loadComposants({ itemsPerPage: 200 }),
|
||||||
|
]).catch(() => {})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -251,78 +251,15 @@
|
|||||||
<p v-else-if="loadingDocuments" class="text-xs text-base-content/70">
|
<p v-else-if="loadingDocuments" class="text-xs text-base-content/70">
|
||||||
Chargement des documents en cours…
|
Chargement des documents en cours…
|
||||||
</p>
|
</p>
|
||||||
<div v-else-if="pieceDocuments.length" class="space-y-2">
|
<DocumentListInline
|
||||||
<div
|
v-else
|
||||||
v-for="document in pieceDocuments"
|
:documents="pieceDocuments"
|
||||||
:key="document.id || document.path || document.name"
|
:can-delete="canEdit"
|
||||||
class="flex items-center justify-between rounded border border-base-200 bg-base-100 px-3 py-2"
|
:delete-disabled="uploadingDocuments"
|
||||||
>
|
empty-text="Aucun document n'est associé à cette pièce pour le moment."
|
||||||
<div class="flex items-center gap-3 text-sm">
|
@preview="openPreview"
|
||||||
<div
|
@delete="removeDocument"
|
||||||
class="flex-shrink-0 overflow-hidden rounded-md border border-base-200 bg-base-200/70 flex items-center justify-center"
|
/>
|
||||||
:class="documentThumbnailClass(document)"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
v-if="isImageDocument(document) && (document.fileUrl || document.path)"
|
|
||||||
:src="document.fileUrl || 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">
|
|
||||||
{{ 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">
|
|
||||||
<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>
|
|
||||||
<button
|
|
||||||
v-if="canEdit"
|
|
||||||
type="button"
|
|
||||||
class="btn btn-error btn-xs"
|
|
||||||
:disabled="uploadingDocuments"
|
|
||||||
@click="removeDocument(document.id)"
|
|
||||||
>
|
|
||||||
Supprimer
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p v-else class="text-xs text-base-content/70">
|
|
||||||
Aucun document n'est associé à cette pièce pour le moment.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<EntityHistorySection
|
<EntityHistorySection
|
||||||
@@ -384,14 +321,6 @@ import {
|
|||||||
requiredCustomFieldsFilled as _requiredCustomFieldsFilled,
|
requiredCustomFieldsFilled as _requiredCustomFieldsFilled,
|
||||||
saveCustomFieldValues as _saveCustomFieldValues,
|
saveCustomFieldValues as _saveCustomFieldValues,
|
||||||
} from '~/shared/utils/customFieldFormUtils'
|
} from '~/shared/utils/customFieldFormUtils'
|
||||||
import {
|
|
||||||
documentIcon,
|
|
||||||
formatSize,
|
|
||||||
shouldInlinePdf,
|
|
||||||
documentPreviewSrc,
|
|
||||||
documentThumbnailClass,
|
|
||||||
downloadDocument,
|
|
||||||
} from '~/shared/utils/documentDisplayUtils'
|
|
||||||
|
|
||||||
interface PieceCatalogType extends ModelType {
|
interface PieceCatalogType extends ModelType {
|
||||||
structure: PieceModelStructure | null
|
structure: PieceModelStructure | null
|
||||||
|
|||||||
@@ -162,74 +162,15 @@
|
|||||||
<p v-else-if="loadingDocuments" class="text-xs text-base-content/70">
|
<p v-else-if="loadingDocuments" class="text-xs text-base-content/70">
|
||||||
Chargement des documents…
|
Chargement des documents…
|
||||||
</p>
|
</p>
|
||||||
<div v-else-if="productDocuments.length" class="space-y-2">
|
<DocumentListInline
|
||||||
<div
|
v-else
|
||||||
v-for="document in productDocuments"
|
:documents="productDocuments"
|
||||||
:key="document.id || document.path || document.name"
|
:can-delete="canEdit"
|
||||||
class="flex items-center justify-between rounded border border-base-200 bg-base-100 px-3 py-2"
|
:delete-disabled="uploadingDocuments || saving"
|
||||||
>
|
empty-text="Aucun document n'est associé à ce produit pour le moment."
|
||||||
<div class="flex items-center gap-3 text-sm">
|
@preview="openPreview"
|
||||||
<div
|
@delete="removeDocument"
|
||||||
class="flex-shrink-0 overflow-hidden rounded-md border border-base-200 bg-base-200/70 flex items-center justify-center"
|
/>
|
||||||
:class="documentThumbnailClass(document)"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
v-if="isImageDocument(document) && (document.fileUrl || document.path)"
|
|
||||||
:src="document.fileUrl || 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">
|
|
||||||
{{ 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">
|
|
||||||
<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>
|
|
||||||
<button
|
|
||||||
v-if="canEdit"
|
|
||||||
type="button"
|
|
||||||
class="btn btn-error btn-xs"
|
|
||||||
:disabled="uploadingDocuments || saving"
|
|
||||||
@click="removeDocument(document.id)"
|
|
||||||
>
|
|
||||||
Supprimer
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p v-else class="text-xs text-base-content/70">
|
|
||||||
Aucun document n'est associé à ce produit pour le moment.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<EntityHistorySection
|
<EntityHistorySection
|
||||||
@@ -290,14 +231,6 @@ import {
|
|||||||
requiredCustomFieldsFilled as _requiredCustomFieldsFilled,
|
requiredCustomFieldsFilled as _requiredCustomFieldsFilled,
|
||||||
saveCustomFieldValues as _saveCustomFieldValues,
|
saveCustomFieldValues as _saveCustomFieldValues,
|
||||||
} from '~/shared/utils/customFieldFormUtils'
|
} from '~/shared/utils/customFieldFormUtils'
|
||||||
import {
|
|
||||||
documentIcon,
|
|
||||||
formatSize,
|
|
||||||
shouldInlinePdf,
|
|
||||||
documentPreviewSrc,
|
|
||||||
documentThumbnailClass,
|
|
||||||
downloadDocument,
|
|
||||||
} from '~/shared/utils/documentDisplayUtils'
|
|
||||||
|
|
||||||
const { canEdit } = usePermissions()
|
const { canEdit } = usePermissions()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|||||||
Reference in New Issue
Block a user