feat: enrich piece assignment labels and document previews
This commit is contained in:
@@ -208,24 +208,33 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div v-if="componentDocuments.length" class="space-y-2">
|
<div v-if="componentDocuments.length" class="space-y-2">
|
||||||
<div
|
<div
|
||||||
v-for="document in componentDocuments"
|
v-for="document in componentDocuments"
|
||||||
:key="document.id"
|
:key="document.id"
|
||||||
class="flex items-center justify-between rounded border border-base-200 bg-base-100 px-3 py-2"
|
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 items-center gap-3 text-sm">
|
||||||
<div class="h-14 w-14 flex-shrink-0 overflow-hidden rounded-md border border-base-200 bg-base-200/70 flex items-center justify-center">
|
<div
|
||||||
<img
|
class="flex-shrink-0 overflow-hidden rounded-md border border-base-200 bg-base-200/70 flex items-center justify-center"
|
||||||
v-if="isImageDocument(document) && document.path"
|
:class="documentThumbnailClass(document)"
|
||||||
:src="document.path"
|
>
|
||||||
class="h-full w-full object-cover"
|
<img
|
||||||
:alt="`Aperçu de ${document.name}`"
|
v-if="isImageDocument(document) && document.path"
|
||||||
>
|
:src="document.path"
|
||||||
<component
|
class="h-full w-full object-cover"
|
||||||
v-else
|
:alt="`Aperçu de ${document.name}`"
|
||||||
:is="documentIcon(document).component"
|
>
|
||||||
class="h-6 w-6"
|
<iframe
|
||||||
:class="documentIcon(document).colorClass"
|
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"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -317,7 +326,7 @@ import DocumentUpload from './DocumentUpload.vue'
|
|||||||
import ConstructeurSelect from './ConstructeurSelect.vue'
|
import ConstructeurSelect from './ConstructeurSelect.vue'
|
||||||
import { useDocuments } from '~/composables/useDocuments'
|
import { useDocuments } from '~/composables/useDocuments'
|
||||||
import { getFileIcon } from '~/utils/fileIcons'
|
import { getFileIcon } from '~/utils/fileIcons'
|
||||||
import { canPreviewDocument, isImageDocument } from '~/utils/documentPreview'
|
import { canPreviewDocument, isImageDocument, isPdfDocument } from '~/utils/documentPreview'
|
||||||
import DocumentPreviewModal from '~/components/DocumentPreviewModal.vue'
|
import DocumentPreviewModal from '~/components/DocumentPreviewModal.vue'
|
||||||
import IconLucideChevronRight from '~icons/lucide/chevron-right'
|
import IconLucideChevronRight from '~icons/lucide/chevron-right'
|
||||||
import { useCustomFields } from '~/composables/useCustomFields'
|
import { useCustomFields } from '~/composables/useCustomFields'
|
||||||
@@ -357,6 +366,40 @@ const componentDocuments = computed(() => props.component.documents || [])
|
|||||||
const documentIcon = doc => getFileIcon({ name: doc.filename || doc.name, mime: doc.mimeType })
|
const documentIcon = doc => getFileIcon({ name: doc.filename || doc.name, mime: doc.mimeType })
|
||||||
const previewDocument = ref(null)
|
const previewDocument = ref(null)
|
||||||
const previewVisible = ref(false)
|
const previewVisible = ref(false)
|
||||||
|
const PDF_PREVIEW_MAX_BYTES = 5 * 1024 * 1024
|
||||||
|
const shouldInlinePdf = (document) => {
|
||||||
|
if (!document || !isPdfDocument(document) || !document.path) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (typeof document.size === 'number' && document.size > PDF_PREVIEW_MAX_BYTES) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
const appendPdfViewerParams = (src) => {
|
||||||
|
if (!src || src.startsWith('data:')) {
|
||||||
|
return src || ''
|
||||||
|
}
|
||||||
|
if (src.includes('#')) {
|
||||||
|
return `${src}&toolbar=0&navpanes=0`
|
||||||
|
}
|
||||||
|
return `${src}#toolbar=0&navpanes=0`
|
||||||
|
}
|
||||||
|
const documentPreviewSrc = (document) => {
|
||||||
|
if (!document?.path) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
if (isPdfDocument(document)) {
|
||||||
|
return appendPdfViewerParams(document.path)
|
||||||
|
}
|
||||||
|
return document.path
|
||||||
|
}
|
||||||
|
const documentThumbnailClass = (document) => {
|
||||||
|
if (shouldInlinePdf(document) || (isImageDocument(document) && document?.path)) {
|
||||||
|
return 'h-24 w-20'
|
||||||
|
}
|
||||||
|
return 'h-16 w-16'
|
||||||
|
}
|
||||||
|
|
||||||
const childComponents = computed(() => {
|
const childComponents = computed(() => {
|
||||||
const list = props.component.subcomponents || props.component.subComponents || []
|
const list = props.component.subcomponents || props.component.subComponents || []
|
||||||
|
|||||||
@@ -228,16 +228,44 @@ watch(
|
|||||||
|
|
||||||
const describePieceRequirement = (definition: ComponentModelPiece) => {
|
const describePieceRequirement = (definition: ComponentModelPiece) => {
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
if (definition.role) {
|
const addPart = (value?: string | null) => {
|
||||||
parts.push(definition.role);
|
const trimmed = typeof value === 'string' ? value.trim() : '';
|
||||||
|
if (trimmed && !parts.includes(trimmed)) {
|
||||||
|
parts.push(trimmed);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const options = getPieceOptions(definition);
|
||||||
|
const fallbackPiece = options[0] || null;
|
||||||
|
const fallbackType = fallbackPiece?.typePiece || null;
|
||||||
|
|
||||||
|
addPart(definition.role);
|
||||||
|
addPart(
|
||||||
|
definition.typePieceLabel ||
|
||||||
|
(definition as any).typePiece?.name ||
|
||||||
|
fallbackType?.name,
|
||||||
|
);
|
||||||
|
|
||||||
|
const family =
|
||||||
|
definition.familyCode ||
|
||||||
|
(definition as any).typePiece?.code ||
|
||||||
|
fallbackType?.code ||
|
||||||
|
null;
|
||||||
|
if (family) {
|
||||||
|
addPart(`Famille ${family}`);
|
||||||
}
|
}
|
||||||
if (definition.typePieceLabel) {
|
|
||||||
parts.push(definition.typePieceLabel);
|
if (parts.length === 0) {
|
||||||
} else if ((definition as any).typePiece?.name) {
|
addPart(fallbackType?.name);
|
||||||
parts.push((definition as any).typePiece.name);
|
if (fallbackType?.code) {
|
||||||
} else if (definition.familyCode) {
|
addPart(`Famille ${fallbackType.code}`);
|
||||||
parts.push(definition.familyCode);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (parts.length === 0 && definition.typePieceId) {
|
||||||
|
addPart(`#${definition.typePieceId}`);
|
||||||
|
}
|
||||||
|
|
||||||
return parts.length ? parts.join(' • ') : 'Pièce du squelette';
|
return parts.length ? parts.join(' • ') : 'Pièce du squelette';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -276,13 +276,22 @@
|
|||||||
class="flex items-center justify-between rounded border border-base-200 bg-base-100 px-3 py-2"
|
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 items-center gap-3 text-sm">
|
||||||
<div class="h-14 w-14 flex-shrink-0 overflow-hidden rounded-md border border-base-200 bg-base-200/70 flex items-center justify-center">
|
<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
|
<img
|
||||||
v-if="isImageDocument(document) && document.path"
|
v-if="isImageDocument(document) && document.path"
|
||||||
:src="document.path"
|
:src="document.path"
|
||||||
class="h-full w-full object-cover"
|
class="h-full w-full object-cover"
|
||||||
:alt="`Aperçu de ${document.name}`"
|
: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
|
<component
|
||||||
v-else
|
v-else
|
||||||
:is="documentIcon(document).component"
|
:is="documentIcon(document).component"
|
||||||
@@ -348,7 +357,7 @@ import { useCustomFields } from "~/composables/useCustomFields";
|
|||||||
import { useToast } from "~/composables/useToast";
|
import { useToast } from "~/composables/useToast";
|
||||||
import { useDocuments } from "~/composables/useDocuments";
|
import { useDocuments } from "~/composables/useDocuments";
|
||||||
import { getFileIcon } from "~/utils/fileIcons";
|
import { getFileIcon } from "~/utils/fileIcons";
|
||||||
import { canPreviewDocument, isImageDocument } from "~/utils/documentPreview";
|
import { canPreviewDocument, isImageDocument, isPdfDocument } from "~/utils/documentPreview";
|
||||||
import DocumentUpload from "~/components/DocumentUpload.vue";
|
import DocumentUpload from "~/components/DocumentUpload.vue";
|
||||||
import DocumentPreviewModal from "~/components/DocumentPreviewModal.vue";
|
import DocumentPreviewModal from "~/components/DocumentPreviewModal.vue";
|
||||||
import IconLucidePackage from "~icons/lucide/package";
|
import IconLucidePackage from "~icons/lucide/package";
|
||||||
@@ -388,6 +397,40 @@ const documentIcon = (doc) =>
|
|||||||
getFileIcon({ name: doc.filename || doc.name, mime: doc.mimeType });
|
getFileIcon({ name: doc.filename || doc.name, mime: doc.mimeType });
|
||||||
const previewDocument = ref(null);
|
const previewDocument = ref(null);
|
||||||
const previewVisible = ref(false);
|
const previewVisible = ref(false);
|
||||||
|
const PDF_PREVIEW_MAX_BYTES = 5 * 1024 * 1024;
|
||||||
|
const shouldInlinePdf = (document) => {
|
||||||
|
if (!document || !isPdfDocument(document) || !document.path) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (typeof document.size === "number" && document.size > PDF_PREVIEW_MAX_BYTES) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
const appendPdfViewerParams = (src) => {
|
||||||
|
if (!src || src.startsWith("data:")) {
|
||||||
|
return src || "";
|
||||||
|
}
|
||||||
|
if (src.includes("#")) {
|
||||||
|
return `${src}&toolbar=0&navpanes=0`;
|
||||||
|
}
|
||||||
|
return `${src}#toolbar=0&navpanes=0`;
|
||||||
|
};
|
||||||
|
const documentPreviewSrc = (document) => {
|
||||||
|
if (!document?.path) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if (isPdfDocument(document)) {
|
||||||
|
return appendPdfViewerParams(document.path);
|
||||||
|
}
|
||||||
|
return document.path;
|
||||||
|
};
|
||||||
|
const documentThumbnailClass = (document) => {
|
||||||
|
if (shouldInlinePdf(document) || (isImageDocument(document) && document?.path)) {
|
||||||
|
return "h-24 w-20";
|
||||||
|
}
|
||||||
|
return "h-16 w-16";
|
||||||
|
};
|
||||||
const extractStructureCustomFields = (structure) => {
|
const extractStructureCustomFields = (structure) => {
|
||||||
if (!structure || typeof structure !== "object") {
|
if (!structure || typeof structure !== "object") {
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
@@ -495,6 +495,8 @@ const addPiece = () => {
|
|||||||
typePieceId: '',
|
typePieceId: '',
|
||||||
typePieceLabel: '',
|
typePieceLabel: '',
|
||||||
reference: '',
|
reference: '',
|
||||||
|
familyCode: '',
|
||||||
|
role: '',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -311,13 +311,22 @@
|
|||||||
class="flex items-center justify-between rounded border border-base-200 bg-base-100 px-3 py-2"
|
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 items-center gap-3 text-sm">
|
||||||
<div class="h-14 w-14 flex-shrink-0 overflow-hidden rounded-md border border-base-200 bg-base-200/70 flex items-center justify-center">
|
<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
|
<img
|
||||||
v-if="isImageDocument(document) && document.path"
|
v-if="isImageDocument(document) && document.path"
|
||||||
:src="document.path"
|
:src="document.path"
|
||||||
class="h-full w-full object-cover"
|
class="h-full w-full object-cover"
|
||||||
:alt="`Aperçu de ${document.name}`"
|
: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
|
<component
|
||||||
v-else
|
v-else
|
||||||
:is="documentIcon(document).component"
|
:is="documentIcon(document).component"
|
||||||
@@ -398,7 +407,7 @@ import { formatStructurePreview, normalizeStructureForEditor } from '~/shared/mo
|
|||||||
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 { getFileIcon } from '~/utils/fileIcons'
|
import { getFileIcon } from '~/utils/fileIcons'
|
||||||
import { canPreviewDocument, isImageDocument } from '~/utils/documentPreview'
|
import { canPreviewDocument, isImageDocument, isPdfDocument } from '~/utils/documentPreview'
|
||||||
|
|
||||||
interface ComponentCatalogType extends ModelType {
|
interface ComponentCatalogType extends ModelType {
|
||||||
structure: ComponentModelStructure | null
|
structure: ComponentModelStructure | null
|
||||||
@@ -458,6 +467,40 @@ const formatSize = (size: number | null | undefined) => {
|
|||||||
const formatted = size / Math.pow(1024, index)
|
const formatted = size / Math.pow(1024, index)
|
||||||
return `${formatted.toFixed(1)} ${units[index]}`
|
return `${formatted.toFixed(1)} ${units[index]}`
|
||||||
}
|
}
|
||||||
|
const PDF_PREVIEW_MAX_BYTES = 5 * 1024 * 1024
|
||||||
|
const shouldInlinePdf = (document: any) => {
|
||||||
|
if (!document || !isPdfDocument(document) || !document.path) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (typeof document.size === 'number' && document.size > PDF_PREVIEW_MAX_BYTES) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
const appendPdfViewerParams = (src: string) => {
|
||||||
|
if (!src || src.startsWith('data:')) {
|
||||||
|
return src || ''
|
||||||
|
}
|
||||||
|
if (src.includes('#')) {
|
||||||
|
return `${src}&toolbar=0&navpanes=0`
|
||||||
|
}
|
||||||
|
return `${src}#toolbar=0&navpanes=0`
|
||||||
|
}
|
||||||
|
const documentPreviewSrc = (document: any) => {
|
||||||
|
if (!document?.path) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
if (isPdfDocument(document)) {
|
||||||
|
return appendPdfViewerParams(document.path)
|
||||||
|
}
|
||||||
|
return document.path
|
||||||
|
}
|
||||||
|
const documentThumbnailClass = (document: any) => {
|
||||||
|
if (shouldInlinePdf(document) || (isImageDocument(document) && document?.path)) {
|
||||||
|
return 'h-24 w-20'
|
||||||
|
}
|
||||||
|
return 'h-16 w-16'
|
||||||
|
}
|
||||||
const openPreview = (doc: any) => {
|
const openPreview = (doc: any) => {
|
||||||
if (!doc || !canPreviewDocument(doc)) {
|
if (!doc || !canPreviewDocument(doc)) {
|
||||||
return
|
return
|
||||||
@@ -944,8 +987,10 @@ const resolvePieceLabel = (piece: Record<string, any>) => {
|
|||||||
parts.push(piece.typePiece.name)
|
parts.push(piece.typePiece.name)
|
||||||
} else if (piece.typePieceLabel) {
|
} else if (piece.typePieceLabel) {
|
||||||
parts.push(piece.typePieceLabel)
|
parts.push(piece.typePieceLabel)
|
||||||
|
} else if (piece.typePiece?.code) {
|
||||||
|
parts.push(`Famille ${piece.typePiece.code}`)
|
||||||
} else if (piece.familyCode) {
|
} else if (piece.familyCode) {
|
||||||
parts.push(piece.familyCode)
|
parts.push(`Famille ${piece.familyCode}`)
|
||||||
} else if (piece.typePieceId) {
|
} else if (piece.typePieceId) {
|
||||||
parts.push(`#${piece.typePieceId}`)
|
parts.push(`#${piece.typePieceId}`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -584,6 +584,7 @@ const sanitizePieceDefinition = (definition: ComponentModelPiece) =>
|
|||||||
typePieceId: definition.typePieceId ?? null,
|
typePieceId: definition.typePieceId ?? null,
|
||||||
typePieceLabel: definition.typePieceLabel ?? null,
|
typePieceLabel: definition.typePieceLabel ?? null,
|
||||||
reference: definition.reference ?? null,
|
reference: definition.reference ?? null,
|
||||||
|
familyCode: (definition as any).familyCode ?? null,
|
||||||
})
|
})
|
||||||
|
|
||||||
const serializeStructureAssignments = (
|
const serializeStructureAssignments = (
|
||||||
@@ -697,8 +698,10 @@ const resolvePieceLabel = (piece: Record<string, any>) => {
|
|||||||
parts.push(piece.typePiece.name)
|
parts.push(piece.typePiece.name)
|
||||||
} else if (piece.typePieceLabel) {
|
} else if (piece.typePieceLabel) {
|
||||||
parts.push(piece.typePieceLabel)
|
parts.push(piece.typePieceLabel)
|
||||||
|
} else if (piece.typePiece?.code) {
|
||||||
|
parts.push(`Famille ${piece.typePiece.code}`)
|
||||||
} else if (piece.familyCode) {
|
} else if (piece.familyCode) {
|
||||||
parts.push(piece.familyCode)
|
parts.push(`Famille ${piece.familyCode}`)
|
||||||
} else if (piece.typePieceId) {
|
} else if (piece.typePieceId) {
|
||||||
parts.push(`#${piece.typePieceId}`)
|
parts.push(`#${piece.typePieceId}`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,9 +86,15 @@
|
|||||||
rounded
|
rounded
|
||||||
>
|
>
|
||||||
<div class="flex justify-center gap-4">
|
<div class="flex justify-center gap-4">
|
||||||
<div class="badge badge-outline">{{ machine.typeMachine?.category || 'N/A' }}</div>
|
<div v-if="machine.typeMachine?.category" class="badge badge-outline">
|
||||||
<div class="badge badge-outline">{{ machine.site?.name }}</div>
|
{{ machine.typeMachine?.category }}
|
||||||
<div v-if="machine.reference" class="badge badge-outline">{{ machine.reference }}</div>
|
</div>
|
||||||
|
<div v-if="machine.site?.name" class="badge badge-outline">
|
||||||
|
{{ machine.site?.name }}
|
||||||
|
</div>
|
||||||
|
<div v-if="machine.reference" class="badge badge-outline">
|
||||||
|
{{ machine.reference }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</PageHero>
|
</PageHero>
|
||||||
|
|
||||||
@@ -113,7 +119,7 @@
|
|||||||
{{ machineName }}
|
{{ machineName }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
<div v-if="isEditMode || machineReference" class="form-control">
|
||||||
<label class="label">
|
<label class="label">
|
||||||
<span class="label-text">Référence</span>
|
<span class="label-text">Référence</span>
|
||||||
</label>
|
</label>
|
||||||
@@ -126,10 +132,10 @@
|
|||||||
@blur="updateMachineInfo"
|
@blur="updateMachineInfo"
|
||||||
/>
|
/>
|
||||||
<div v-else class="input input-bordered bg-base-200">
|
<div v-else class="input input-bordered bg-base-200">
|
||||||
{{ machineReference || 'Non définie' }}
|
{{ machineReference }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
<div v-if="isEditMode || hasMachineConstructeur" class="form-control">
|
||||||
<label class="label">
|
<label class="label">
|
||||||
<span class="label-text">Constructeur</span>
|
<span class="label-text">Constructeur</span>
|
||||||
</label>
|
</label>
|
||||||
@@ -143,9 +149,11 @@
|
|||||||
/>
|
/>
|
||||||
<div v-else class="input input-bordered bg-base-200">
|
<div v-else class="input input-bordered bg-base-200">
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<span class="font-medium">{{ machineConstructeurDisplay?.name || 'Non défini' }}</span>
|
<span class="font-medium">
|
||||||
<span class="text-xs text-gray-500">
|
{{ machineConstructeurDisplay?.name || machineConstructeurContact }}
|
||||||
{{ [machineConstructeurDisplay?.email, machineConstructeurDisplay?.phone].filter(Boolean).join(' • ') || '' }}
|
</span>
|
||||||
|
<span v-if="machineConstructeurContact" class="text-xs text-gray-500">
|
||||||
|
{{ machineConstructeurContact }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -153,11 +161,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Champs personnalisés -->
|
<!-- Champs personnalisés -->
|
||||||
<div v-if="machineCustomFields.length" class="mt-6 pt-4 border-t border-gray-200">
|
<div v-if="visibleMachineCustomFields.length" class="mt-6 pt-4 border-t border-gray-200">
|
||||||
<h4 class="font-semibold text-gray-700 mb-3">Champs personnalisés de la machine</h4>
|
<h4 class="font-semibold text-gray-700 mb-3">Champs personnalisés de la machine</h4>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div
|
<div
|
||||||
v-for="field in machineCustomFields"
|
v-for="field in visibleMachineCustomFields"
|
||||||
:key="field.customFieldValueId || field.id || field.name"
|
:key="field.customFieldValueId || field.id || field.name"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
>
|
>
|
||||||
@@ -265,13 +273,22 @@
|
|||||||
class="flex items-center justify-between rounded border border-base-200 bg-base-100 px-3 py-2"
|
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 items-center gap-3 text-sm">
|
||||||
<div class="h-14 w-14 flex-shrink-0 overflow-hidden rounded-md border border-base-200 bg-base-200/70 flex items-center justify-center">
|
<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
|
<img
|
||||||
v-if="isImageDocument(document) && document.path"
|
v-if="isImageDocument(document) && document.path"
|
||||||
:src="document.path"
|
:src="document.path"
|
||||||
class="h-full w-full object-cover"
|
class="h-full w-full object-cover"
|
||||||
:alt="`Aperçu de ${document.name}`"
|
: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
|
<component
|
||||||
v-else
|
v-else
|
||||||
:is="documentIcon(document).component"
|
:is="documentIcon(document).component"
|
||||||
@@ -525,7 +542,7 @@ import { useToast } from '~/composables/useToast'
|
|||||||
import { useDocuments } from '~/composables/useDocuments'
|
import { useDocuments } from '~/composables/useDocuments'
|
||||||
import { getFileIcon } from '~/utils/fileIcons'
|
import { getFileIcon } from '~/utils/fileIcons'
|
||||||
import { sanitizeDefinitionOverrides, normalizeStructureForEditor } from '~/shared/modelUtils'
|
import { sanitizeDefinitionOverrides, normalizeStructureForEditor } from '~/shared/modelUtils'
|
||||||
import { canPreviewDocument, isImageDocument } from '~/utils/documentPreview'
|
import { canPreviewDocument, isImageDocument, isPdfDocument } from '~/utils/documentPreview'
|
||||||
import ComponentHierarchy from '~/components/ComponentHierarchy.vue'
|
import ComponentHierarchy from '~/components/ComponentHierarchy.vue'
|
||||||
import DocumentUpload from '~/components/DocumentUpload.vue'
|
import DocumentUpload from '~/components/DocumentUpload.vue'
|
||||||
import ConstructeurSelect from '~/components/ConstructeurSelect.vue'
|
import ConstructeurSelect from '~/components/ConstructeurSelect.vue'
|
||||||
@@ -595,6 +612,20 @@ const machineConstructeurDisplay = computed(() => {
|
|||||||
if (!id) return machine.value?.constructeur || null
|
if (!id) return machine.value?.constructeur || null
|
||||||
return constructeurs.value.find(item => item.id === id) || machine.value?.constructeur || null
|
return constructeurs.value.find(item => item.id === id) || machine.value?.constructeur || null
|
||||||
})
|
})
|
||||||
|
const machineConstructeurContact = computed(() => {
|
||||||
|
const constructeur = machineConstructeurDisplay.value
|
||||||
|
if (!constructeur) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
return [constructeur.email, constructeur.phone].filter(Boolean).join(' • ')
|
||||||
|
})
|
||||||
|
const hasMachineConstructeur = computed(() => {
|
||||||
|
const constructeur = machineConstructeurDisplay.value
|
||||||
|
if (!constructeur) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return Boolean(constructeur.name || machineConstructeurContact.value)
|
||||||
|
})
|
||||||
|
|
||||||
const machineDocumentFiles = ref([])
|
const machineDocumentFiles = ref([])
|
||||||
const machineDocumentsUploading = ref(false)
|
const machineDocumentsUploading = ref(false)
|
||||||
@@ -1630,6 +1661,45 @@ const formatSize = (size) => {
|
|||||||
return `${formatted.toFixed(1)} ${units[index]}`
|
return `${formatted.toFixed(1)} ${units[index]}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const PDF_PREVIEW_MAX_BYTES = 5 * 1024 * 1024
|
||||||
|
|
||||||
|
const shouldInlinePdf = (document) => {
|
||||||
|
if (!document || !isPdfDocument(document) || !document.path) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (typeof document.size === 'number' && document.size > PDF_PREVIEW_MAX_BYTES) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const appendPdfViewerParams = (src) => {
|
||||||
|
if (!src || src.startsWith('data:')) {
|
||||||
|
return src || ''
|
||||||
|
}
|
||||||
|
if (src.includes('#')) {
|
||||||
|
return `${src}&toolbar=0&navpanes=0`
|
||||||
|
}
|
||||||
|
return `${src}#toolbar=0&navpanes=0`
|
||||||
|
}
|
||||||
|
|
||||||
|
const documentPreviewSrc = (document) => {
|
||||||
|
if (!document?.path) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
if (isPdfDocument(document)) {
|
||||||
|
return appendPdfViewerParams(document.path)
|
||||||
|
}
|
||||||
|
return document.path
|
||||||
|
}
|
||||||
|
|
||||||
|
const documentThumbnailClass = (document) => {
|
||||||
|
if (shouldInlinePdf(document) || (isImageDocument(document) && document?.path)) {
|
||||||
|
return 'h-24 w-20'
|
||||||
|
}
|
||||||
|
return 'h-16 w-16'
|
||||||
|
}
|
||||||
|
|
||||||
const formatCustomFieldValue = (field) => {
|
const formatCustomFieldValue = (field) => {
|
||||||
if (!field) {
|
if (!field) {
|
||||||
return 'Non défini'
|
return 'Non défini'
|
||||||
@@ -1674,6 +1744,14 @@ const shouldDisplayCustomField = (field) => {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const visibleMachineCustomFields = computed(() => {
|
||||||
|
const fields = Array.isArray(machineCustomFields.value) ? machineCustomFields.value : []
|
||||||
|
if (isEditMode.value) {
|
||||||
|
return fields
|
||||||
|
}
|
||||||
|
return fields.filter((field) => shouldDisplayCustomField(field))
|
||||||
|
})
|
||||||
|
|
||||||
const summarizeCustomFields = (fields = []) => {
|
const summarizeCustomFields = (fields = []) => {
|
||||||
const seen = new Set()
|
const seen = new Set()
|
||||||
return fields
|
return fields
|
||||||
|
|||||||
@@ -270,13 +270,22 @@
|
|||||||
class="flex items-center justify-between rounded border border-base-200 bg-base-100 px-3 py-2"
|
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 items-center gap-3 text-sm">
|
||||||
<div class="h-14 w-14 flex-shrink-0 overflow-hidden rounded-md border border-base-200 bg-base-200/70 flex items-center justify-center">
|
<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
|
<img
|
||||||
v-if="isImageDocument(document) && document.path"
|
v-if="isImageDocument(document) && document.path"
|
||||||
:src="document.path"
|
:src="document.path"
|
||||||
class="h-full w-full object-cover"
|
class="h-full w-full object-cover"
|
||||||
:alt="`Aperçu de ${document.name}`"
|
: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
|
<component
|
||||||
v-else
|
v-else
|
||||||
:is="documentIcon(document).component"
|
:is="documentIcon(document).component"
|
||||||
@@ -354,7 +363,7 @@ import { useApi } from '~/composables/useApi'
|
|||||||
import { useToast } from '~/composables/useToast'
|
import { useToast } from '~/composables/useToast'
|
||||||
import { useDocuments } from '~/composables/useDocuments'
|
import { useDocuments } from '~/composables/useDocuments'
|
||||||
import { getFileIcon } from '~/utils/fileIcons'
|
import { getFileIcon } from '~/utils/fileIcons'
|
||||||
import { canPreviewDocument, isImageDocument } from '~/utils/documentPreview'
|
import { canPreviewDocument, isImageDocument, isPdfDocument } from '~/utils/documentPreview'
|
||||||
import { formatPieceStructurePreview } from '~/shared/modelUtils'
|
import { formatPieceStructurePreview } from '~/shared/modelUtils'
|
||||||
import type { PieceModelStructure } from '~/shared/types/inventory'
|
import type { PieceModelStructure } from '~/shared/types/inventory'
|
||||||
import type { ModelType } from '~/services/modelTypes'
|
import type { ModelType } from '~/services/modelTypes'
|
||||||
@@ -417,6 +426,40 @@ const formatSize = (size: number | null | undefined) => {
|
|||||||
const formatted = size / Math.pow(1024, index)
|
const formatted = size / Math.pow(1024, index)
|
||||||
return `${formatted.toFixed(1)} ${units[index]}`
|
return `${formatted.toFixed(1)} ${units[index]}`
|
||||||
}
|
}
|
||||||
|
const PDF_PREVIEW_MAX_BYTES = 5 * 1024 * 1024
|
||||||
|
const shouldInlinePdf = (document: any) => {
|
||||||
|
if (!document || !isPdfDocument(document) || !document.path) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (typeof document.size === 'number' && document.size > PDF_PREVIEW_MAX_BYTES) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
const appendPdfViewerParams = (src: string) => {
|
||||||
|
if (!src || src.startsWith('data:')) {
|
||||||
|
return src || ''
|
||||||
|
}
|
||||||
|
if (src.includes('#')) {
|
||||||
|
return `${src}&toolbar=0&navpanes=0`
|
||||||
|
}
|
||||||
|
return `${src}#toolbar=0&navpanes=0`
|
||||||
|
}
|
||||||
|
const documentPreviewSrc = (document: any) => {
|
||||||
|
if (!document?.path) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
if (isPdfDocument(document)) {
|
||||||
|
return appendPdfViewerParams(document.path)
|
||||||
|
}
|
||||||
|
return document.path
|
||||||
|
}
|
||||||
|
const documentThumbnailClass = (document: any) => {
|
||||||
|
if (shouldInlinePdf(document) || (isImageDocument(document) && document?.path)) {
|
||||||
|
return 'h-24 w-20'
|
||||||
|
}
|
||||||
|
return 'h-16 w-16'
|
||||||
|
}
|
||||||
const openPreview = (doc: any) => {
|
const openPreview = (doc: any) => {
|
||||||
if (!doc || !canPreviewDocument(doc)) {
|
if (!doc || !canPreviewDocument(doc)) {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -202,11 +202,27 @@ const sanitizePieces = (pieces: any[]): ComponentModelPiece[] => {
|
|||||||
? piece.reference.trim()
|
? piece.reference.trim()
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
if (!typePieceId && !typePieceLabel && !reference) {
|
const rawFamilyCode = typeof piece?.familyCode === 'string'
|
||||||
|
? piece.familyCode.trim()
|
||||||
|
: typeof piece?.typePiece?.code === 'string'
|
||||||
|
? piece.typePiece.code.trim()
|
||||||
|
: ''
|
||||||
|
const familyCode = rawFamilyCode.length > 0 ? rawFamilyCode : undefined
|
||||||
|
|
||||||
|
const rawRole = typeof piece?.role === 'string' ? piece.role.trim() : ''
|
||||||
|
const role = rawRole.length > 0 ? rawRole : undefined
|
||||||
|
|
||||||
|
if (!typePieceId && !typePieceLabel && !reference && !familyCode) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const result: ComponentModelPiece = {}
|
const result: ComponentModelPiece = {}
|
||||||
|
if (role) {
|
||||||
|
result.role = role
|
||||||
|
}
|
||||||
|
if (familyCode) {
|
||||||
|
result.familyCode = familyCode
|
||||||
|
}
|
||||||
if (reference !== undefined) {
|
if (reference !== undefined) {
|
||||||
result.reference = reference
|
result.reference = reference
|
||||||
}
|
}
|
||||||
@@ -519,6 +535,8 @@ const hydratePieces = (pieces: any[]): ComponentModelPiece[] => {
|
|||||||
typePieceId: piece?.typePieceId ?? piece?.typePiece?.id ?? '',
|
typePieceId: piece?.typePieceId ?? piece?.typePiece?.id ?? '',
|
||||||
typePieceLabel: piece?.typePieceLabel ?? piece?.typePiece?.name ?? '',
|
typePieceLabel: piece?.typePieceLabel ?? piece?.typePiece?.name ?? '',
|
||||||
reference: piece?.reference ?? '',
|
reference: piece?.reference ?? '',
|
||||||
|
familyCode: piece?.familyCode ?? piece?.typePiece?.code ?? '',
|
||||||
|
role: piece?.role ?? '',
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -590,6 +608,8 @@ const mapComponentPieces = (pieces: any[]): ComponentModelPiece[] => {
|
|||||||
reference: piece?.reference ?? '',
|
reference: piece?.reference ?? '',
|
||||||
typePieceId: piece?.typePieceId ?? piece?.typePiece?.id ?? '',
|
typePieceId: piece?.typePieceId ?? piece?.typePiece?.id ?? '',
|
||||||
typePieceLabel: piece?.typePieceLabel ?? piece?.typePiece?.name ?? '',
|
typePieceLabel: piece?.typePieceLabel ?? piece?.typePiece?.name ?? '',
|
||||||
|
familyCode: piece?.familyCode ?? piece?.typePiece?.code ?? '',
|
||||||
|
role: piece?.role ?? '',
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ export interface ComponentModelPiece {
|
|||||||
typePieceId?: string
|
typePieceId?: string
|
||||||
typePieceLabel?: string
|
typePieceLabel?: string
|
||||||
reference?: string
|
reference?: string
|
||||||
|
familyCode?: string
|
||||||
|
role?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ComponentModelStructureNode {
|
export interface ComponentModelStructureNode {
|
||||||
@@ -119,9 +121,11 @@ const validatePiece = (
|
|||||||
const typePieceId = ensureString(value.typePieceId)
|
const typePieceId = ensureString(value.typePieceId)
|
||||||
const typePieceLabel = ensureString(value.typePieceLabel)
|
const typePieceLabel = ensureString(value.typePieceLabel)
|
||||||
const reference = ensureString(value.reference)
|
const reference = ensureString(value.reference)
|
||||||
|
const familyCode = ensureString((value as any).familyCode)
|
||||||
|
const role = ensureString((value as any).role)
|
||||||
|
|
||||||
if (!typePieceId && !typePieceLabel && !reference) {
|
if (!typePieceId && !typePieceLabel && !reference && !familyCode) {
|
||||||
issues.push(`${path}: au moins un identifiant ou libellé de pièce est requis`)
|
issues.push(`${path}: au moins un identifiant, une famille ou une référence de pièce est requis`)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,6 +133,8 @@ const validatePiece = (
|
|||||||
...(typePieceId ? { typePieceId } : {}),
|
...(typePieceId ? { typePieceId } : {}),
|
||||||
...(typePieceLabel ? { typePieceLabel } : {}),
|
...(typePieceLabel ? { typePieceLabel } : {}),
|
||||||
...(reference ? { reference } : {}),
|
...(reference ? { reference } : {}),
|
||||||
|
...(familyCode ? { familyCode } : {}),
|
||||||
|
...(role ? { role } : {}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ export const canPreviewDocument = (document = {}) => !!getPreviewType(document)
|
|||||||
|
|
||||||
export const isImageDocument = (document = {}) => getPreviewType(document) === 'image'
|
export const isImageDocument = (document = {}) => getPreviewType(document) === 'image'
|
||||||
|
|
||||||
|
export const isPdfDocument = (document = {}) => getPreviewType(document) === 'pdf'
|
||||||
|
|
||||||
export const describeDocument = (document) => {
|
export const describeDocument = (document) => {
|
||||||
if (!document) { return '' }
|
if (!document) { return '' }
|
||||||
const name = document.filename || document.name || ''
|
const name = document.filename || document.name || ''
|
||||||
|
|||||||
Reference in New Issue
Block a user