4309 lines
137 KiB
Vue
4309 lines
137 KiB
Vue
<template>
|
|
<main class="container mx-auto px-6 py-8">
|
|
<!-- Loading State -->
|
|
<div v-if="loading" class="flex justify-center items-center py-12">
|
|
<span class="loading loading-spinner loading-lg"></span>
|
|
</div>
|
|
|
|
<!-- Machine Details -->
|
|
<div v-else-if="machine" ref="printAreaRef" class="space-y-8">
|
|
<DocumentPreviewModal
|
|
:document="previewDocument"
|
|
:visible="previewVisible"
|
|
@close="closePreview"
|
|
/>
|
|
|
|
<!-- Header with actions -->
|
|
<div class="flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
|
|
<div class="flex flex-col gap-2">
|
|
<h1 class="text-3xl font-bold">
|
|
{{ machineViewTitle }}
|
|
</h1>
|
|
<div class="btn-group w-full max-w-xs print:hidden" data-print-hide>
|
|
<button
|
|
type="button"
|
|
class="btn btn-sm"
|
|
:class="isDetailsView ? 'btn-primary' : 'btn-outline'"
|
|
@click="changeMachineView('details')"
|
|
>
|
|
Vue machine
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="btn btn-sm"
|
|
:class="isSkeletonView ? 'btn-primary' : 'btn-outline'"
|
|
:disabled="!machineHasSkeletonRequirements"
|
|
@click="changeMachineView('skeleton')"
|
|
>
|
|
Squelette
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center gap-2 print:hidden" data-print-hide>
|
|
<button
|
|
@click="toggleEditMode"
|
|
class="btn btn-primary"
|
|
:class="{ 'btn-outline': isEditMode }"
|
|
>
|
|
<IconLucideSquarePen
|
|
v-if="!isEditMode"
|
|
class="w-5 h-5 mr-2"
|
|
aria-hidden="true"
|
|
/>
|
|
<IconLucideEye
|
|
v-else
|
|
class="w-5 h-5 mr-2"
|
|
aria-hidden="true"
|
|
/>
|
|
{{ isEditMode ? 'Voir détails' : 'Modifier' }}
|
|
</button>
|
|
<button
|
|
v-if="isDetailsView && !isEditMode"
|
|
@click="openPrintModal"
|
|
type="button"
|
|
class="btn btn-outline btn-secondary"
|
|
>
|
|
<IconLucidePrinter class="w-5 h-5 mr-2" aria-hidden="true" />
|
|
Imprimer
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<template v-if="isDetailsView">
|
|
<!-- Debug info -->
|
|
<div v-if="debug" class="bg-yellow-100 p-4 rounded-lg">
|
|
<p>Debug: Machine trouvée - {{ machine.name }}</p>
|
|
<p>Components count: {{ components.length }}</p>
|
|
<p>Pieces count: {{ pieces.length }}</p>
|
|
</div>
|
|
|
|
<!-- Hero -->
|
|
<PageHero
|
|
:title="machine.name"
|
|
:subtitle="machine.description || machine.typeMachine?.description"
|
|
min-height="min-h-[20vh]"
|
|
max-width="max-w-md"
|
|
rounded
|
|
>
|
|
<div class="flex justify-center gap-4">
|
|
<div v-if="machine.typeMachine?.category" class="badge badge-outline">
|
|
{{ machine.typeMachine?.category }}
|
|
</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>
|
|
</PageHero>
|
|
|
|
<!-- Machine Info Card -->
|
|
<div class="card bg-base-100 shadow-lg">
|
|
<div class="card-body">
|
|
<h2 class="card-title">Informations de la machine</h2>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div class="form-control">
|
|
<label class="label">
|
|
<span class="label-text">Nom</span>
|
|
</label>
|
|
<input
|
|
v-if="isEditMode"
|
|
:id="getMachineFieldId('name')"
|
|
v-model="machineName"
|
|
type="text"
|
|
class="input input-bordered"
|
|
@blur="updateMachineInfo"
|
|
/>
|
|
<div v-else class="input input-bordered bg-base-200">
|
|
{{ machineName }}
|
|
</div>
|
|
</div>
|
|
<div v-if="isEditMode || machineReference" class="form-control">
|
|
<label class="label">
|
|
<span class="label-text">Référence</span>
|
|
</label>
|
|
<input
|
|
v-if="isEditMode"
|
|
:id="getMachineFieldId('reference')"
|
|
v-model="machineReference"
|
|
type="text"
|
|
class="input input-bordered"
|
|
@blur="updateMachineInfo"
|
|
/>
|
|
<div v-else class="input input-bordered bg-base-200">
|
|
{{ machineReference }}
|
|
</div>
|
|
</div>
|
|
<div v-if="isEditMode || hasMachineConstructeur" class="form-control">
|
|
<label class="label">
|
|
<span class="label-text">Fournisseur</span>
|
|
</label>
|
|
<ConstructeurSelect
|
|
v-if="isEditMode"
|
|
class="w-full"
|
|
:key="machine.value?.id"
|
|
:model-value="machineConstructeurIds"
|
|
:initial-options="machineConstructeursDisplay"
|
|
placeholder="Rechercher un ou plusieurs fournisseurs..."
|
|
@update:modelValue="handleMachineConstructeurChange"
|
|
/>
|
|
<div v-else class="input input-bordered bg-base-200">
|
|
<div v-if="machineConstructeursDisplay.length" class="space-y-1">
|
|
<div
|
|
v-for="constructeur in machineConstructeursDisplay"
|
|
:key="constructeur.id"
|
|
class="flex flex-col"
|
|
>
|
|
<span class="font-medium">{{ constructeur.name }}</span>
|
|
<span
|
|
v-if="formatConstructeurContactSummary(constructeur)"
|
|
class="text-xs text-gray-500"
|
|
>
|
|
{{ formatConstructeurContactSummary(constructeur) }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<span v-else class="font-medium">Non défini</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Champs personnalisés -->
|
|
<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>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div
|
|
v-for="field in visibleMachineCustomFields"
|
|
:key="field.customFieldValueId || field.id || field.name"
|
|
class="form-control"
|
|
>
|
|
<label class="label">
|
|
<span class="label-text text-sm">{{ field.name }}</span>
|
|
<span v-if="field.required" class="label-text-alt text-error">*</span>
|
|
</label>
|
|
|
|
<template v-if="isEditMode">
|
|
<input
|
|
v-if="field.type === 'text'"
|
|
:value="field.value ?? ''"
|
|
type="text"
|
|
class="input input-bordered input-sm"
|
|
:required="field.required"
|
|
@input="setMachineCustomFieldValue(field, $event.target.value)"
|
|
@blur="updateMachineCustomField(field)"
|
|
/>
|
|
<input
|
|
v-else-if="field.type === 'number'"
|
|
:value="field.value ?? ''"
|
|
type="number"
|
|
class="input input-bordered input-sm"
|
|
:required="field.required"
|
|
@input="setMachineCustomFieldValue(field, $event.target.value)"
|
|
@blur="updateMachineCustomField(field)"
|
|
/>
|
|
<select
|
|
v-else-if="field.type === 'select'"
|
|
:value="field.value ?? ''"
|
|
class="select select-bordered select-sm"
|
|
:required="field.required"
|
|
@change="setMachineCustomFieldValue(field, $event.target.value)"
|
|
@blur="updateMachineCustomField(field)"
|
|
>
|
|
<option value="">Sélectionner...</option>
|
|
<option
|
|
v-for="option in field.options"
|
|
:key="option"
|
|
:value="option"
|
|
>
|
|
{{ option }}
|
|
</option>
|
|
</select>
|
|
<div v-else-if="field.type === 'boolean'" class="flex items-center gap-2">
|
|
<input
|
|
:value="field.value ?? ''"
|
|
type="checkbox"
|
|
class="checkbox checkbox-sm"
|
|
:checked="String(field.value).toLowerCase() === 'true'"
|
|
@change="setMachineCustomFieldValue(field, $event.target.checked ? 'true' : 'false')"
|
|
@blur="updateMachineCustomField(field)"
|
|
/>
|
|
<span class="text-sm">{{ String(field.value).toLowerCase() === 'true' ? 'Oui' : 'Non' }}</span>
|
|
</div>
|
|
<input
|
|
v-else-if="field.type === 'date'"
|
|
:value="field.value ?? ''"
|
|
type="date"
|
|
class="input input-bordered input-sm"
|
|
:required="field.required"
|
|
@input="setMachineCustomFieldValue(field, $event.target.value)"
|
|
@blur="updateMachineCustomField(field)"
|
|
/>
|
|
<div v-else class="text-xs text-error">
|
|
Type de champ non pris en charge
|
|
</div>
|
|
</template>
|
|
<template v-else>
|
|
<div class="input input-bordered input-sm bg-base-200">
|
|
{{ formatCustomFieldValue(field) }}
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Documents -->
|
|
<div class="card bg-base-100 shadow-lg mt-6">
|
|
<div class="card-body space-y-4">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<h2 class="card-title">Documents de la machine</h2>
|
|
<p class="text-xs text-gray-500">Ajoutez ou consultez les documents liés à cette machine.</p>
|
|
</div>
|
|
<span v-if="isEditMode && machineDocumentFiles.length" class="badge badge-outline">
|
|
{{ machineDocumentFiles.length }} fichier{{ machineDocumentFiles.length > 1 ? 's' : '' }} sélectionné{{ machineDocumentFiles.length > 1 ? 's' : '' }}
|
|
</span>
|
|
</div>
|
|
|
|
<DocumentUpload
|
|
v-if="isEditMode"
|
|
v-model="machineDocumentFiles"
|
|
title="Déposer des fichiers pour la machine"
|
|
subtitle="Formats acceptés : PDF, images, documents..."
|
|
@files-added="handleMachineFilesAdded"
|
|
/>
|
|
|
|
<div v-if="machineDocumentsList.length" class="space-y-2">
|
|
<div
|
|
v-for="document in machineDocumentsList"
|
|
:key="document.id"
|
|
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.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">{{ 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="machineDocumentsUploading"
|
|
@click="removeMachineDocument(document.id)"
|
|
>
|
|
Supprimer
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<p v-else class="text-xs text-gray-500">Aucun document lié à cette machine.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Produits associés -->
|
|
<div class="card bg-base-100 shadow-lg">
|
|
<div class="card-body space-y-4">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<h2 class="card-title">Produits associés</h2>
|
|
<p class="text-xs text-gray-500">
|
|
Produits sélectionnés directement pour cette machine selon le squelette.
|
|
</p>
|
|
</div>
|
|
<span class="badge badge-outline" v-if="machineDirectProducts.length">
|
|
{{ machineDirectProducts.length }} produit{{ machineDirectProducts.length > 1 ? 's' : '' }}
|
|
</span>
|
|
</div>
|
|
|
|
<div v-if="machineDirectProducts.length" class="space-y-3">
|
|
<div
|
|
v-for="product in machineDirectProducts"
|
|
:key="product.id || product.name"
|
|
class="rounded border border-base-200 bg-base-200/60 p-3 text-sm space-y-1"
|
|
>
|
|
<div class="flex items-center justify-between flex-wrap gap-2">
|
|
<p class="font-semibold text-base-content">
|
|
{{ product.name }}
|
|
</p>
|
|
<span class="badge badge-ghost badge-sm">
|
|
{{ product.groupLabel }}
|
|
</span>
|
|
</div>
|
|
<p v-if="product.reference" class="text-xs text-base-content/70">
|
|
<span class="font-medium">Référence :</span>
|
|
<span class="ml-1">{{ product.reference }}</span>
|
|
</p>
|
|
<p v-if="product.supplierLabel" class="text-xs text-base-content/70">
|
|
<span class="font-medium">Fournisseurs :</span>
|
|
<span class="ml-1">{{ product.supplierLabel }}</span>
|
|
</p>
|
|
<p v-if="product.priceLabel" class="text-xs text-base-content/70">
|
|
<span class="font-medium">Prix indicatif :</span>
|
|
<span class="ml-1">{{ product.priceLabel }}</span>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<p v-else class="text-xs text-gray-500">
|
|
Aucun produit n'a été associé directement à cette machine.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Components Section -->
|
|
<div class="card bg-base-100 shadow-lg">
|
|
<div class="card-body">
|
|
<div class="flex justify-between items-center mb-4">
|
|
<h2 class="card-title">Composants</h2>
|
|
<button
|
|
type="button"
|
|
class="btn btn-ghost btn-sm gap-2"
|
|
@click="toggleAllComponents"
|
|
:title="componentsCollapsed ? 'Déplier tous les composants' : 'Replier tous les composants'"
|
|
>
|
|
<IconLucideChevronRight
|
|
class="w-5 h-5 transition-transform"
|
|
:class="componentsCollapsed ? 'rotate-0' : 'rotate-90'"
|
|
aria-hidden="true"
|
|
/>
|
|
<span class="text-sm">
|
|
{{ componentsCollapsed ? 'Tout déplier' : 'Tout replier' }}
|
|
</span>
|
|
</button>
|
|
</div>
|
|
|
|
<ComponentHierarchy
|
|
:components="components"
|
|
:is-edit-mode="isEditMode"
|
|
:collapse-all="componentsCollapsed"
|
|
:toggle-token="collapseToggleToken"
|
|
@update="updateComponent"
|
|
@edit-piece="updatePieceFromComponent"
|
|
@custom-field-update="updatePieceCustomField"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Machine Pieces Section -->
|
|
<div class="card bg-base-100 shadow-lg">
|
|
<div class="card-body">
|
|
<div class="flex justify-between items-center mb-4">
|
|
<h2 class="card-title">Pièces de la machine</h2>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
<PieceItem
|
|
v-for="piece in machinePieces"
|
|
:key="piece.id"
|
|
:piece="piece"
|
|
:is-edit-mode="isEditMode"
|
|
@update="updatePieceInfo"
|
|
@edit="editPiece"
|
|
@custom-field-update="updatePieceCustomField"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<template v-else>
|
|
<div
|
|
v-if="componentRequirementGroups.length || pieceRequirementGroups.length || productRequirementGroups.length"
|
|
class="card bg-base-100 shadow-lg"
|
|
>
|
|
<div class="card-body space-y-6">
|
|
<div>
|
|
<h2 class="card-title">Structure sélectionnée</h2>
|
|
<p class="text-sm text-gray-500">
|
|
Synthèse des familles définies dans le type et des modèles utilisés pour cette machine.
|
|
</p>
|
|
</div>
|
|
|
|
<div v-if="componentRequirementGroups.length" class="space-y-4">
|
|
<h3 class="text-sm font-semibold text-gray-700">Composants</h3>
|
|
<div
|
|
v-for="group in componentRequirementGroups"
|
|
:key="group.requirement.id"
|
|
class="rounded-lg border border-base-200 p-4"
|
|
>
|
|
<div class="flex flex-wrap items-center justify-between gap-2 mb-3">
|
|
<div>
|
|
<h4 class="font-medium text-sm">
|
|
{{ group.requirement.label || group.requirement.typeComposant?.name || 'Famille de composants' }}
|
|
</h4>
|
|
<p class="text-xs text-gray-500">
|
|
Type : {{ group.requirement.typeComposant?.name || 'Non défini' }} · Min {{ group.requirement.minCount ?? (group.requirement.required ? 1 : 0) }} · Max {{ group.requirement.maxCount ?? '∞' }}
|
|
</p>
|
|
</div>
|
|
<span class="badge badge-outline badge-sm">{{ group.components.length }} composant(s)</span>
|
|
</div>
|
|
|
|
<div v-if="group.components.length" class="space-y-2">
|
|
<div
|
|
v-for="component in group.components"
|
|
:key="component.id"
|
|
class="flex flex-wrap items-center gap-2 text-sm"
|
|
>
|
|
<span class="font-medium">{{ component.name }}</span>
|
|
<span v-if="component.parentComposantId" class="text-xs text-gray-500">
|
|
(Sous-composant)
|
|
</span>
|
|
<div
|
|
v-if="summarizeCustomFields(component.customFields || []).length"
|
|
class="w-full flex flex-wrap gap-2 text-xs text-gray-600"
|
|
>
|
|
<span
|
|
v-for="field in summarizeCustomFields(component.customFields || [])"
|
|
:key="field.key"
|
|
class="badge badge-ghost badge-sm whitespace-pre-wrap"
|
|
>
|
|
<span class="font-medium">{{ field.label }} :</span>
|
|
<span class="ml-1">{{ field.value }}</span>
|
|
</span>
|
|
</div>
|
|
<div
|
|
v-if="component.__productDisplay"
|
|
class="w-full text-xs text-gray-600 space-y-1"
|
|
>
|
|
<div>
|
|
<span class="font-medium">Produit :</span>
|
|
<span>{{ component.__productDisplay.name }}</span>
|
|
</div>
|
|
<div v-if="component.__productDisplay.category">
|
|
<span class="font-medium">Catégorie :</span>
|
|
<span>{{ component.__productDisplay.category }}</span>
|
|
</div>
|
|
<div v-if="component.__productDisplay.reference">
|
|
<span class="font-medium">Référence :</span>
|
|
<span>{{ component.__productDisplay.reference }}</span>
|
|
</div>
|
|
<div v-if="component.__productDisplay.suppliers">
|
|
<span class="font-medium">Fournisseurs :</span>
|
|
<span>{{ component.__productDisplay.suppliers }}</span>
|
|
</div>
|
|
<div v-if="component.__productDisplay.price">
|
|
<span class="font-medium">Prix indicatif :</span>
|
|
<span>{{ component.__productDisplay.price }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<p v-else class="text-xs text-gray-500">Aucun composant rattaché à ce groupe.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="pieceRequirementGroups.length" class="space-y-4">
|
|
<h3 class="text-sm font-semibold text-gray-700">Pièces principales</h3>
|
|
<div
|
|
v-for="group in pieceRequirementGroups"
|
|
:key="group.requirement.id"
|
|
class="rounded-lg border border-base-200 p-4"
|
|
>
|
|
<div class="flex flex-wrap items-center justify-between gap-2 mb-3">
|
|
<div>
|
|
<h4 class="font-medium text-sm">
|
|
{{ group.requirement.label || group.requirement.typePiece?.name || 'Groupe de pièces' }}
|
|
</h4>
|
|
<p class="text-xs text-gray-500">
|
|
Type : {{ group.requirement.typePiece?.name || 'Non défini' }} · Min {{ group.requirement.minCount ?? (group.requirement.required ? 1 : 0) }} · Max {{ group.requirement.maxCount ?? '∞' }}
|
|
</p>
|
|
</div>
|
|
<span class="badge badge-outline badge-sm">{{ group.pieces.length }} pièce(s)</span>
|
|
</div>
|
|
|
|
<div v-if="group.pieces.length" class="space-y-2">
|
|
<div
|
|
v-for="piece in group.pieces"
|
|
:key="piece.id"
|
|
class="flex flex-wrap items-center gap-2 text-sm"
|
|
>
|
|
<span class="font-medium">{{ piece.name }}</span>
|
|
<span v-if="piece.parentComponentName" class="text-xs text-gray-500">
|
|
(Rattachée à {{ piece.parentComponentName }})
|
|
</span>
|
|
<div
|
|
v-if="summarizeCustomFields(piece.customFields || []).length"
|
|
class="w-full flex flex-wrap gap-2 text-xs text-gray-600"
|
|
>
|
|
<span
|
|
v-for="field in summarizeCustomFields(piece.customFields || [])"
|
|
:key="field.key"
|
|
class="badge badge-ghost badge-sm whitespace-pre-wrap"
|
|
>
|
|
<span class="font-medium">{{ field.label }} :</span>
|
|
<span class="ml-1">{{ field.value }}</span>
|
|
</span>
|
|
</div>
|
|
<div
|
|
v-if="piece.__productDisplay"
|
|
class="w-full text-xs text-gray-600 space-y-1"
|
|
>
|
|
<div>
|
|
<span class="font-medium">Produit :</span>
|
|
<span>{{ piece.__productDisplay.name }}</span>
|
|
</div>
|
|
<div v-if="piece.__productDisplay.category">
|
|
<span class="font-medium">Catégorie :</span>
|
|
<span>{{ piece.__productDisplay.category }}</span>
|
|
</div>
|
|
<div v-if="piece.__productDisplay.reference">
|
|
<span class="font-medium">Référence :</span>
|
|
<span>{{ piece.__productDisplay.reference }}</span>
|
|
</div>
|
|
<div v-if="piece.__productDisplay.suppliers">
|
|
<span class="font-medium">Fournisseurs :</span>
|
|
<span>{{ piece.__productDisplay.suppliers }}</span>
|
|
</div>
|
|
<div v-if="piece.__productDisplay.price">
|
|
<span class="font-medium">Prix indicatif :</span>
|
|
<span>{{ piece.__productDisplay.price }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<p v-else class="text-xs text-gray-500">Aucune pièce rattachée à ce groupe.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="productRequirementGroups.length" class="space-y-4">
|
|
<h3 class="text-sm font-semibold text-gray-700">Produits requis</h3>
|
|
<div
|
|
v-for="group in productRequirementGroups"
|
|
:key="group.requirement.id"
|
|
class="rounded-lg border border-base-200 p-4"
|
|
>
|
|
<div class="flex flex-wrap items-center justify-between gap-2 mb-3">
|
|
<div>
|
|
<h4 class="font-medium text-sm">
|
|
{{ group.requirement.label || group.requirement.typeProduct?.name || 'Groupe de produits' }}
|
|
</h4>
|
|
<p class="text-xs text-gray-500">
|
|
Catégorie : {{ group.requirement.typeProduct?.name || 'Non définie' }} · Min {{ group.requirement.minCount ?? (group.requirement.required ? 1 : 0) }} · Max {{ group.requirement.maxCount ?? '∞' }}
|
|
</p>
|
|
</div>
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
<span class="badge badge-outline badge-sm">Total {{ group.totalCount }}</span>
|
|
<span class="badge badge-ghost badge-sm">Direct {{ group.directProducts.length }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="text-xs text-gray-500 mb-3">
|
|
Via composants : {{ group.componentCount }} • Via pièces : {{ group.pieceCount }}
|
|
</div>
|
|
|
|
<div v-if="group.directProducts.length" class="space-y-2">
|
|
<div
|
|
v-for="product in group.directProducts"
|
|
:key="product.id || product.name"
|
|
class="rounded border border-base-200 bg-base-200/60 p-3 text-sm"
|
|
>
|
|
<div class="font-medium">
|
|
{{ product.name }}
|
|
</div>
|
|
<div v-if="product.reference" class="text-xs text-gray-500">
|
|
Référence : {{ product.reference }}
|
|
</div>
|
|
<div v-if="product.supplierLabel" class="text-xs text-gray-500">
|
|
Fournisseurs : {{ product.supplierLabel }}
|
|
</div>
|
|
<div v-if="product.priceLabel" class="text-xs text-gray-500">
|
|
Prix indicatif : {{ product.priceLabel }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<p v-else class="text-xs text-gray-500">
|
|
Aucune sélection directe. Couverture assurée via composants ou pièces associés.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<!-- Error State -->
|
|
<div v-else class="text-center py-12">
|
|
<div class="max-w-md mx-auto">
|
|
<IconLucideAlertTriangle class="w-16 h-16 mx-auto text-gray-400 mb-4" aria-hidden="true" />
|
|
<h3 class="text-lg font-medium text-gray-900 mb-2">Machine non trouvée</h3>
|
|
<p class="text-gray-500 mb-4">La machine avec l'ID "{{ machineId }}" n'existe pas ou a été supprimée.</p>
|
|
<NuxtLink to="/machines" class="btn btn-primary">
|
|
Retour aux machines
|
|
</NuxtLink>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
<MachinePrintSelectionModal
|
|
:open="printModalOpen"
|
|
:selection="printSelection"
|
|
:components="components"
|
|
:pieces="machinePieces"
|
|
@close="closePrintModal"
|
|
@confirm="handlePrintConfirm"
|
|
@select-all="setAllPrintSelection(true)"
|
|
@deselect-all="setAllPrintSelection(false)"
|
|
/>
|
|
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, reactive, onMounted, computed, watch, nextTick } from 'vue'
|
|
import { useRoute } from 'vue-router'
|
|
import { useMachines } from '~/composables/useMachines'
|
|
import { useComposants } from '~/composables/useComposants'
|
|
import { usePieces } from '~/composables/usePieces'
|
|
import { useComponentTypes } from '~/composables/useComponentTypes'
|
|
import { usePieceTypes } from '~/composables/usePieceTypes'
|
|
import { useCustomFields } from '~/composables/useCustomFields'
|
|
import { useApi } from '~/composables/useApi'
|
|
import { useToast } from '~/composables/useToast'
|
|
import { useDocuments } from '~/composables/useDocuments'
|
|
import { getFileIcon } from '~/utils/fileIcons'
|
|
import { sanitizeDefinitionOverrides, normalizeStructureForEditor } from '~/shared/modelUtils'
|
|
import {
|
|
resolveConstructeurs,
|
|
uniqueConstructeurIds,
|
|
formatConstructeurContact as formatConstructeurContactSummary,
|
|
} from '~/shared/constructeurUtils'
|
|
import { canPreviewDocument, isImageDocument, isPdfDocument } from '~/utils/documentPreview'
|
|
import ComponentHierarchy from '~/components/ComponentHierarchy.vue'
|
|
import DocumentUpload from '~/components/DocumentUpload.vue'
|
|
import ConstructeurSelect from '~/components/ConstructeurSelect.vue'
|
|
import { useConstructeurs } from '~/composables/useConstructeurs'
|
|
import { useProducts } from '~/composables/useProducts'
|
|
import DocumentPreviewModal from '~/components/DocumentPreviewModal.vue'
|
|
import PageHero from '~/components/PageHero.vue'
|
|
import MachinePrintSelectionModal from '~/components/MachinePrintSelectionModal.vue'
|
|
import { buildMachinePrintContext, buildMachinePrintHtml } from '~/utils/printTemplates/machineReport'
|
|
import IconLucideAlertTriangle from '~icons/lucide/alert-triangle'
|
|
import IconLucideSquarePen from '~icons/lucide/square-pen'
|
|
import IconLucideEye from '~icons/lucide/eye'
|
|
import IconLucideChevronRight from '~icons/lucide/chevron-right'
|
|
import IconLucidePrinter from '~icons/lucide/printer'
|
|
import IconLucidePlus from '~icons/lucide/plus'
|
|
import IconLucideX from '~icons/lucide/x'
|
|
|
|
const route = useRoute()
|
|
const machineId = route.params.id
|
|
|
|
// Vérifier que l'ID est valide
|
|
if (!machineId) {
|
|
console.error('ID de machine manquant')
|
|
}
|
|
|
|
// Composables
|
|
const {
|
|
updateMachine: updateMachineApi,
|
|
reconfigureSkeleton: reconfigureMachineSkeleton,
|
|
} = useMachines()
|
|
const {
|
|
updateComposant: updateComposantApi
|
|
} = useComposants()
|
|
const {
|
|
updatePiece: updatePieceApi
|
|
} = usePieces()
|
|
const { componentTypes, loadComponentTypes } = useComponentTypes()
|
|
const { pieceTypes, loadPieceTypes } = usePieceTypes()
|
|
const { products, loadProducts } = useProducts()
|
|
|
|
const { upsertCustomFieldValue, updateCustomFieldValue: updateCustomFieldValueApi } = useCustomFields()
|
|
const { get } = useApi()
|
|
const {
|
|
uploadDocuments,
|
|
deleteDocument,
|
|
loadDocumentsByMachine,
|
|
loadDocumentsByComponent,
|
|
loadDocumentsByPiece
|
|
} = useDocuments()
|
|
const toast = useToast()
|
|
|
|
// Data
|
|
const loading = ref(true)
|
|
const machine = ref(null)
|
|
const components = ref([])
|
|
const pieces = ref([])
|
|
const machineComponentLinks = ref([])
|
|
const machinePieceLinks = ref([])
|
|
const machineProductLinks = ref([])
|
|
const printAreaRef = ref(null)
|
|
|
|
const { constructeurs, loadConstructeurs } = useConstructeurs()
|
|
|
|
// Champs de la machine
|
|
const machineName = ref('')
|
|
const machineReference = ref('')
|
|
const machineConstructeurIds = ref([])
|
|
const machineConstructeurId = computed({
|
|
get: () => machineConstructeurIds.value[0] || null,
|
|
set: (value) => {
|
|
machineConstructeurIds.value = value ? [value] : []
|
|
},
|
|
})
|
|
const machineConstructeursDisplay = computed(() => {
|
|
const ids = uniqueConstructeurIds(
|
|
machineConstructeurIds.value,
|
|
machine.value?.constructeurIds,
|
|
machine.value?.constructeurs,
|
|
machine.value?.constructeur,
|
|
)
|
|
return resolveConstructeurs(
|
|
ids,
|
|
Array.isArray(machine.value?.constructeurs) ? machine.value?.constructeurs : [],
|
|
machine.value?.constructeur ? [machine.value.constructeur] : [],
|
|
constructeurs.value,
|
|
)
|
|
})
|
|
const machineConstructeurContact = computed(() =>
|
|
machineConstructeursDisplay.value
|
|
.map((constructeur) => formatConstructeurContactSummary(constructeur))
|
|
.filter(Boolean)
|
|
.join(' • '),
|
|
)
|
|
const hasMachineConstructeur = computed(
|
|
() => machineConstructeursDisplay.value.length > 0,
|
|
)
|
|
|
|
const machineDocumentFiles = ref([])
|
|
const machineDocumentsUploading = ref(false)
|
|
const machineDocumentsLoaded = ref(false)
|
|
const machineCustomFields = ref([])
|
|
const previewDocument = ref(null)
|
|
const previewVisible = ref(false)
|
|
const printModalOpen = ref(false)
|
|
const printSelection = reactive({
|
|
machine: {
|
|
info: true,
|
|
customFields: true,
|
|
documents: true,
|
|
},
|
|
components: {},
|
|
pieces: {},
|
|
})
|
|
|
|
const activeMachineView = ref('details')
|
|
const isDetailsView = computed(() => activeMachineView.value === 'details')
|
|
const isSkeletonView = computed(() => activeMachineView.value === 'skeleton')
|
|
|
|
const skeletonEditor = reactive({
|
|
open: false,
|
|
loading: false,
|
|
submitting: false,
|
|
})
|
|
|
|
const componentRequirementSelections = reactive({})
|
|
const pieceRequirementSelections = reactive({})
|
|
const productRequirementSelections = reactive({})
|
|
|
|
const machineType = computed(() => machine.value?.typeMachine || null)
|
|
const componentRequirements = computed(() => machineType.value?.componentRequirements || [])
|
|
const pieceRequirements = computed(() => machineType.value?.pieceRequirements || [])
|
|
const productRequirements = computed(() => machineType.value?.productRequirements || [])
|
|
const machineHasSkeletonRequirements = computed(() =>
|
|
componentRequirements.value.length > 0 ||
|
|
pieceRequirements.value.length > 0 ||
|
|
productRequirements.value.length > 0
|
|
)
|
|
|
|
const componentTypeOptions = computed(() => componentTypes.value || [])
|
|
const pieceTypeOptions = computed(() => pieceTypes.value || [])
|
|
|
|
const componentTypeLabelMap = computed(() => {
|
|
const map = new Map()
|
|
componentTypeOptions.value.forEach((type) => {
|
|
if (type?.id) {
|
|
map.set(type.id, type.name || '')
|
|
}
|
|
})
|
|
componentRequirements.value.forEach((requirement) => {
|
|
const type = requirement.typeComposant
|
|
if (type?.id) {
|
|
map.set(type.id, type.name || '')
|
|
}
|
|
})
|
|
return map
|
|
})
|
|
|
|
const pieceTypeLabelMap = computed(() => {
|
|
const map = new Map()
|
|
pieceTypeOptions.value.forEach((type) => {
|
|
if (type?.id) {
|
|
map.set(type.id, type.name || '')
|
|
}
|
|
})
|
|
pieceRequirements.value.forEach((requirement) => {
|
|
const type = requirement.typePiece
|
|
if (type?.id) {
|
|
map.set(type.id, type.name || '')
|
|
}
|
|
})
|
|
return map
|
|
})
|
|
|
|
const productInventory = computed(() => products.value || [])
|
|
|
|
const productById = computed(() => {
|
|
const map = new Map()
|
|
productInventory.value.forEach((product) => {
|
|
if (product?.id) {
|
|
map.set(product.id, product)
|
|
}
|
|
})
|
|
return map
|
|
})
|
|
|
|
const findProductById = (productId) => {
|
|
if (!productId) {
|
|
return null
|
|
}
|
|
return productById.value.get(productId) || null
|
|
}
|
|
|
|
const resolveProductReference = (source) => {
|
|
if (!source || typeof source !== 'object') {
|
|
return { product: null, productId: null }
|
|
}
|
|
|
|
const candidateKeys = [
|
|
null,
|
|
'productLink',
|
|
'machinePieceLink',
|
|
'machineComponentLink',
|
|
'machineProductLink',
|
|
'originalPiece',
|
|
'originalComposant',
|
|
'link',
|
|
'overrides',
|
|
'machineComponentLinkOverrides',
|
|
'requirement',
|
|
'selection',
|
|
'entry',
|
|
]
|
|
|
|
let product = null
|
|
let productId = null
|
|
|
|
const inspect = (container) => {
|
|
if (!container || typeof container !== 'object') {
|
|
return
|
|
}
|
|
if (!product && container.product && typeof container.product === 'object') {
|
|
product = container.product
|
|
}
|
|
if (!productId) {
|
|
const candidate =
|
|
container.productId ||
|
|
(container.product && typeof container.product === 'object'
|
|
? container.product.id || container.product.productId
|
|
: null) ||
|
|
null
|
|
if (candidate) {
|
|
productId = candidate
|
|
}
|
|
}
|
|
}
|
|
|
|
candidateKeys.forEach((key) => {
|
|
if (key === null) {
|
|
inspect(source)
|
|
} else {
|
|
inspect(source[key])
|
|
}
|
|
})
|
|
|
|
if (!product && productId) {
|
|
product = findProductById(productId) || null
|
|
}
|
|
|
|
if (!product && !productId && source.productName) {
|
|
const suppliersLabel = typeof source.constructeursLabel === 'string'
|
|
? source.constructeursLabel
|
|
: typeof source.productSuppliers === 'string'
|
|
? source.productSuppliers
|
|
: null
|
|
|
|
return {
|
|
product: {
|
|
name: source.productName,
|
|
reference: source.productReference || null,
|
|
typeProduct: source.productCategory
|
|
? { name: source.productCategory }
|
|
: null,
|
|
constructeurs: suppliersLabel
|
|
? suppliersLabel
|
|
.split(',')
|
|
.map((name) => name.trim())
|
|
.filter((name) => name.length > 0)
|
|
.map((name) => ({ name }))
|
|
: undefined,
|
|
supplierPrice:
|
|
source.productPrice ??
|
|
source.productPriceLabel ??
|
|
source.price ??
|
|
null,
|
|
},
|
|
productId: null,
|
|
}
|
|
}
|
|
|
|
if (productId && product && product.id && product.id !== productId) {
|
|
const resolved = findProductById(productId)
|
|
if (resolved) {
|
|
product = resolved
|
|
}
|
|
}
|
|
|
|
return { product: product || null, productId: productId || null }
|
|
}
|
|
|
|
const getProductSuppliersLabel = (product) => {
|
|
if (!product) {
|
|
return null
|
|
}
|
|
const suppliers = Array.isArray(product.constructeurs)
|
|
? product.constructeurs.map((constructeur) => constructeur?.name).filter(Boolean)
|
|
: []
|
|
if (suppliers.length > 0) {
|
|
return suppliers.join(', ')
|
|
}
|
|
return null
|
|
}
|
|
|
|
const getProductPriceLabel = (product) => {
|
|
if (!product) {
|
|
return null
|
|
}
|
|
const priceValue =
|
|
product.supplierPrice ??
|
|
product.prix ??
|
|
product.price ??
|
|
null
|
|
if (priceValue === undefined || priceValue === null) {
|
|
return null
|
|
}
|
|
const numeric = Number(priceValue)
|
|
if (Number.isNaN(numeric)) {
|
|
return null
|
|
}
|
|
return `${numeric.toFixed(2)} €`
|
|
}
|
|
|
|
const resolveProductFromSource = (source) => resolveProductReference(source).product
|
|
|
|
const getProductDisplay = (source) => {
|
|
if (!source || typeof source !== 'object') {
|
|
return null
|
|
}
|
|
|
|
const { product, productId } = resolveProductReference(source)
|
|
|
|
if (product) {
|
|
return {
|
|
name: product.name || product.reference || 'Produit catalogue',
|
|
reference: product.reference || null,
|
|
category: product.typeProduct?.name || null,
|
|
suppliers: getProductSuppliersLabel(product),
|
|
price: getProductPriceLabel(product),
|
|
}
|
|
}
|
|
|
|
let fallbackName =
|
|
source.productName ||
|
|
source.productLabel ||
|
|
source.typeProductLabel ||
|
|
source.typeProduct?.name ||
|
|
(productId ? `Produit ${productId}` : null)
|
|
let fallbackReference =
|
|
source.productReference ||
|
|
source.reference ||
|
|
null
|
|
let fallbackCategory =
|
|
source.productCategory ||
|
|
source.typeProductLabel ||
|
|
source.typeProduct?.name ||
|
|
null
|
|
let fallbackSuppliers =
|
|
source.productSuppliers ||
|
|
source.constructeursLabel ||
|
|
source.supplierLabel ||
|
|
null
|
|
let fallbackPrice =
|
|
source.productPriceLabel ||
|
|
source.productPrice ||
|
|
source.priceLabel ||
|
|
source.price ||
|
|
null
|
|
|
|
const structuralCandidates = [
|
|
source.products,
|
|
source.productSkeleton,
|
|
source.definition?.products,
|
|
source.definition?.productSkeleton,
|
|
source.definition?.structure?.products,
|
|
source.definition?.structure?.productSkeleton,
|
|
source.structure?.products,
|
|
source.structure?.productSkeleton,
|
|
source.requirement?.products,
|
|
source.requirement?.productSkeleton,
|
|
source.requirement?.structure?.products,
|
|
source.requirement?.structure?.productSkeleton,
|
|
source.requirement?.componentSkeleton?.products,
|
|
source.typeMachineComponentRequirement?.products,
|
|
source.typeMachineComponentRequirement?.productSkeleton,
|
|
source.typeMachineComponentRequirement?.structure?.products,
|
|
source.typeMachineComponentRequirement?.structure?.productSkeleton,
|
|
source.typeMachineComponentRequirement?.componentSkeleton?.products,
|
|
source.typeComposant?.products,
|
|
source.typeComposant?.productSkeleton,
|
|
source.typeComposant?.structure?.products,
|
|
source.typeComposant?.structure?.productSkeleton,
|
|
source.originalComposant?.products,
|
|
source.originalComposant?.productSkeleton,
|
|
source.originalComposant?.definition?.products,
|
|
source.originalComposant?.definition?.productSkeleton,
|
|
source.originalComposant?.definition?.structure?.products,
|
|
source.originalComposant?.definition?.structure?.productSkeleton,
|
|
source.originalComponent?.products,
|
|
source.originalComponent?.productSkeleton,
|
|
source.originalComponent?.definition?.products,
|
|
source.originalComponent?.definition?.productSkeleton,
|
|
source.originalComponent?.definition?.structure?.products,
|
|
source.originalComponent?.definition?.structure?.productSkeleton,
|
|
]
|
|
|
|
const structuralProducts = structuralCandidates
|
|
.flatMap((candidate) => {
|
|
if (Array.isArray(candidate)) {
|
|
return candidate
|
|
}
|
|
if (candidate && typeof candidate === 'object' && Array.isArray(candidate.products)) {
|
|
return candidate.products
|
|
}
|
|
return []
|
|
})
|
|
.filter((entry) => entry && typeof entry === 'object')
|
|
|
|
const structuralProduct = structuralProducts.length ? structuralProducts[0] : null
|
|
|
|
const structuralFamilyCode =
|
|
(structuralProduct && typeof structuralProduct.familyCode === 'string'
|
|
? structuralProduct.familyCode
|
|
: null) ||
|
|
(typeof source.familyCode === 'string' ? source.familyCode : null)
|
|
|
|
if (!fallbackName && structuralProduct) {
|
|
fallbackName =
|
|
structuralProduct.typeProductLabel ||
|
|
structuralProduct.typeProduct?.name ||
|
|
structuralProduct.reference ||
|
|
(structuralFamilyCode ? `Famille ${structuralFamilyCode}` : null) ||
|
|
null
|
|
}
|
|
|
|
if (!fallbackReference && structuralProduct?.reference) {
|
|
fallbackReference = structuralProduct.reference
|
|
}
|
|
|
|
if (!fallbackCategory) {
|
|
fallbackCategory =
|
|
structuralProduct?.typeProductLabel ||
|
|
structuralProduct?.typeProduct?.name ||
|
|
(structuralFamilyCode ? `Famille ${structuralFamilyCode}` : null) ||
|
|
null
|
|
}
|
|
|
|
if (!fallbackSuppliers && structuralProduct?.supplierLabel) {
|
|
fallbackSuppliers = structuralProduct.supplierLabel
|
|
}
|
|
|
|
if (!fallbackSuppliers && Array.isArray(structuralProduct?.constructeurs)) {
|
|
const supplierNames = structuralProduct.constructeurs
|
|
.map((constructeur) => constructeur?.name)
|
|
.filter((name) => typeof name === 'string' && name.trim().length > 0)
|
|
if (supplierNames.length) {
|
|
fallbackSuppliers = supplierNames.join(', ')
|
|
}
|
|
}
|
|
|
|
if (!fallbackPrice && structuralProduct?.priceLabel) {
|
|
fallbackPrice = structuralProduct.priceLabel
|
|
}
|
|
if (!fallbackPrice && structuralProduct?.price) {
|
|
fallbackPrice = structuralProduct.price
|
|
}
|
|
|
|
if (
|
|
fallbackName ||
|
|
fallbackReference ||
|
|
fallbackCategory ||
|
|
fallbackSuppliers ||
|
|
fallbackPrice
|
|
) {
|
|
return {
|
|
name: fallbackName || 'Produit catalogue',
|
|
reference: fallbackReference,
|
|
category: fallbackCategory,
|
|
suppliers: fallbackSuppliers,
|
|
price: typeof fallbackPrice === 'number'
|
|
? `${fallbackPrice.toFixed(2)} €`
|
|
: fallbackPrice || null,
|
|
}
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
const getProductOptionsForRequirement = (requirement) => {
|
|
const requirementTypeId =
|
|
requirement?.typeProductId ||
|
|
requirement?.typeProduct?.id ||
|
|
null
|
|
|
|
return productInventory.value.filter((product) => {
|
|
if (!product?.id) {
|
|
return false
|
|
}
|
|
if (!requirementTypeId) {
|
|
return true
|
|
}
|
|
const productTypeId =
|
|
product.typeProductId ||
|
|
product.typeProduct?.id ||
|
|
null
|
|
return productTypeId === requirementTypeId
|
|
})
|
|
}
|
|
|
|
const resolveProductRequirementTypeLabel = (requirement, entry) => {
|
|
const typeId =
|
|
entry?.typeProductId ||
|
|
requirement?.typeProductId ||
|
|
requirement?.typeProduct?.id ||
|
|
null
|
|
|
|
if (typeId) {
|
|
const typeMatch = productRequirements.value.find(
|
|
(req) => req.typeProductId === typeId || req.typeProduct?.id === typeId,
|
|
)
|
|
if (typeMatch?.typeProduct?.name) {
|
|
return typeMatch.typeProduct.name
|
|
}
|
|
}
|
|
return requirement?.typeProduct?.name || 'Catégorie non définie'
|
|
}
|
|
|
|
const isPlainObject = (value) => Object.prototype.toString.call(value) === '[object Object]'
|
|
|
|
const resolveIdentifier = (...candidates) => {
|
|
for (const candidate of candidates) {
|
|
if (candidate !== undefined && candidate !== null && candidate !== '') {
|
|
return candidate
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
const extractParentLinkIdentifiers = (source) => {
|
|
if (!source || typeof source !== 'object') {
|
|
return {}
|
|
}
|
|
|
|
const identifiers = {}
|
|
|
|
const idKeys = [
|
|
'parentRequirementId',
|
|
'parentComponentRequirementId',
|
|
'parentPieceRequirementId',
|
|
'parentMachineComponentRequirementId',
|
|
'parentMachinePieceRequirementId',
|
|
'parentLinkId',
|
|
'parentComponentLinkId',
|
|
'parentPieceLinkId',
|
|
'parentComponentId',
|
|
'parentPieceId',
|
|
]
|
|
|
|
idKeys.forEach((key) => {
|
|
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|
const value = source[key]
|
|
if (value !== undefined && value !== null && value !== '') {
|
|
identifiers[key] = value
|
|
}
|
|
}
|
|
})
|
|
|
|
const objectKeys = [
|
|
'parentRequirement',
|
|
'parentComponentRequirement',
|
|
'parentPieceRequirement',
|
|
'parentMachineComponentRequirement',
|
|
'parentMachinePieceRequirement',
|
|
]
|
|
|
|
objectKeys.forEach((key) => {
|
|
const value = source[key]
|
|
if (isPlainObject(value) && value.id !== undefined && value.id !== null && value.id !== '') {
|
|
const idKey = `${key}Id`
|
|
if (!Object.prototype.hasOwnProperty.call(identifiers, idKey)) {
|
|
identifiers[idKey] = value.id
|
|
}
|
|
}
|
|
})
|
|
|
|
return identifiers
|
|
}
|
|
|
|
const resolveComponentRequirementTypeLabel = (requirement, entry) => {
|
|
const typeId = entry?.typeComposantId || requirement?.typeComposantId || null
|
|
if (!typeId) {
|
|
return requirement?.typeComposant?.name || 'Type non défini'
|
|
}
|
|
return componentTypeLabelMap.value.get(typeId) || requirement?.typeComposant?.name || 'Type non défini'
|
|
}
|
|
|
|
const resolvePieceRequirementTypeLabel = (requirement, entry) => {
|
|
const typeId = entry?.typePieceId || requirement?.typePieceId || null
|
|
if (!typeId) {
|
|
return requirement?.typePiece?.name || 'Type non défini'
|
|
}
|
|
return pieceTypeLabelMap.value.get(typeId) || requirement?.typePiece?.name || 'Type non défini'
|
|
}
|
|
|
|
const getComponentRequirementEntries = (requirementId) => {
|
|
return componentRequirementSelections[requirementId] || []
|
|
}
|
|
|
|
const getPieceRequirementEntries = (requirementId) => {
|
|
return pieceRequirementSelections[requirementId] || []
|
|
}
|
|
|
|
const getProductRequirementEntries = (requirementId) => {
|
|
return productRequirementSelections[requirementId] || []
|
|
}
|
|
|
|
const createComponentSelectionEntry = (requirement, source = null) => {
|
|
const link = source?.machineComponentLink || null
|
|
|
|
const entry = {
|
|
linkId: resolveIdentifier(link?.id, source?.machineComponentLinkId, source?.linkId),
|
|
composantId: resolveIdentifier(source?.composantId, source?.componentId, source?.id),
|
|
parentLinkId: resolveIdentifier(
|
|
link?.parentLinkId,
|
|
link?.parentComponentLinkId,
|
|
source?.parentComponentLinkId,
|
|
source?.parentLinkId,
|
|
),
|
|
parentRequirementId: resolveIdentifier(
|
|
link?.parentRequirementId,
|
|
source?.parentRequirementId,
|
|
requirement?.parentRequirementId,
|
|
),
|
|
parentMachineComponentRequirementId: resolveIdentifier(
|
|
link?.parentMachineComponentRequirementId,
|
|
source?.parentMachineComponentRequirementId,
|
|
requirement?.parentMachineComponentRequirementId,
|
|
),
|
|
parentMachinePieceRequirementId: resolveIdentifier(
|
|
link?.parentMachinePieceRequirementId,
|
|
source?.parentMachinePieceRequirementId,
|
|
requirement?.parentMachinePieceRequirementId,
|
|
),
|
|
parentComponentId: resolveIdentifier(link?.parentComponentId, source?.parentComponentId),
|
|
parentPieceId: resolveIdentifier(link?.parentPieceId, source?.parentPieceId),
|
|
typeComposantId:
|
|
source?.typeMachineComponentRequirement?.typeComposantId
|
|
|| source?.typeComposantId
|
|
|| source?.typeComposant?.id
|
|
|| requirement?.typeComposantId
|
|
|| null,
|
|
definition: {
|
|
name:
|
|
source?.name
|
|
|| source?.nom
|
|
|| requirement?.typeComposant?.name
|
|
|| '',
|
|
reference: source?.reference || '',
|
|
constructeurIds: [],
|
|
constructeurId: null,
|
|
prix:
|
|
source?.prix
|
|
?? source?.price
|
|
?? null,
|
|
},
|
|
}
|
|
|
|
const definitionConstructeurIds = uniqueConstructeurIds(
|
|
link?.overrides?.constructeurIds,
|
|
link?.overrides?.constructeurId,
|
|
source?.constructeurIds,
|
|
source?.constructeurId,
|
|
source?.constructeur,
|
|
)
|
|
entry.definition.constructeurIds = definitionConstructeurIds
|
|
entry.definition.constructeurId = definitionConstructeurIds[0] || null
|
|
|
|
if (link?.overrides && isPlainObject(link.overrides)) {
|
|
entry.definition = {
|
|
...entry.definition,
|
|
...link.overrides,
|
|
}
|
|
}
|
|
|
|
const finalizedConstructeurIds = uniqueConstructeurIds(
|
|
entry.definition.constructeurIds,
|
|
entry.definition.constructeurId,
|
|
entry.definition.constructeur,
|
|
)
|
|
entry.definition.constructeurIds = finalizedConstructeurIds
|
|
entry.definition.constructeurId = finalizedConstructeurIds[0] || null
|
|
|
|
return entry
|
|
}
|
|
|
|
const createPieceSelectionEntry = (requirement, source = null) => {
|
|
const link = source?.machinePieceLink || null
|
|
|
|
const entry = {
|
|
linkId: resolveIdentifier(link?.id, source?.machinePieceLinkId, source?.linkId),
|
|
pieceId: resolveIdentifier(source?.pieceId, source?.id),
|
|
parentLinkId: resolveIdentifier(link?.parentLinkId, source?.parentLinkId),
|
|
parentComponentLinkId: resolveIdentifier(
|
|
link?.parentComponentLinkId,
|
|
source?.parentComponentLinkId,
|
|
source?.machineComponentLinkId,
|
|
),
|
|
parentRequirementId: resolveIdentifier(
|
|
link?.parentRequirementId,
|
|
source?.parentRequirementId,
|
|
requirement?.parentRequirementId,
|
|
),
|
|
parentMachineComponentRequirementId: resolveIdentifier(
|
|
link?.parentMachineComponentRequirementId,
|
|
source?.parentMachineComponentRequirementId,
|
|
requirement?.parentMachineComponentRequirementId,
|
|
),
|
|
parentMachinePieceRequirementId: resolveIdentifier(
|
|
link?.parentMachinePieceRequirementId,
|
|
source?.parentMachinePieceRequirementId,
|
|
requirement?.parentMachinePieceRequirementId,
|
|
),
|
|
parentComponentId: resolveIdentifier(link?.parentComponentId, source?.parentComponentId, source?.composantId),
|
|
parentPieceId: resolveIdentifier(link?.parentPieceId, source?.parentPieceId),
|
|
composantId: resolveIdentifier(source?.composantId, link?.composantId, link?.componentId),
|
|
typePieceId:
|
|
source?.typeMachinePieceRequirement?.typePieceId
|
|
|| source?.typePieceId
|
|
|| source?.typePiece?.id
|
|
|| requirement?.typePieceId
|
|
|| null,
|
|
definition: {
|
|
name:
|
|
source?.name
|
|
|| source?.nom
|
|
|| requirement?.typePiece?.name
|
|
|| '',
|
|
reference: source?.reference || '',
|
|
constructeurIds: [],
|
|
constructeurId: null,
|
|
prix:
|
|
source?.prix
|
|
?? source?.price
|
|
?? null,
|
|
},
|
|
}
|
|
|
|
const definitionConstructeurIds = uniqueConstructeurIds(
|
|
link?.overrides?.constructeurIds,
|
|
link?.overrides?.constructeurId,
|
|
source?.constructeurIds,
|
|
source?.constructeurId,
|
|
source?.constructeur,
|
|
)
|
|
entry.definition.constructeurIds = definitionConstructeurIds
|
|
entry.definition.constructeurId = definitionConstructeurIds[0] || null
|
|
|
|
if (link?.overrides && isPlainObject(link.overrides)) {
|
|
entry.definition = {
|
|
...entry.definition,
|
|
...link.overrides,
|
|
}
|
|
}
|
|
|
|
const finalizedConstructeurIds = uniqueConstructeurIds(
|
|
entry.definition.constructeurIds,
|
|
entry.definition.constructeurId,
|
|
entry.definition.constructeur,
|
|
)
|
|
entry.definition.constructeurIds = finalizedConstructeurIds
|
|
entry.definition.constructeurId = finalizedConstructeurIds[0] || null
|
|
|
|
return entry
|
|
}
|
|
|
|
const createProductSelectionEntry = (requirement, source = null) => {
|
|
const link = source?.machineProductLink || source || null
|
|
|
|
return {
|
|
linkId: resolveIdentifier(
|
|
link?.id,
|
|
source?.machineProductLinkId,
|
|
source?.linkId,
|
|
),
|
|
productId: resolveIdentifier(source?.productId, link?.productId),
|
|
parentLinkId: resolveIdentifier(link?.parentLinkId, source?.parentLinkId),
|
|
parentComponentLinkId: resolveIdentifier(
|
|
link?.parentComponentLinkId,
|
|
source?.parentComponentLinkId,
|
|
),
|
|
parentPieceLinkId: resolveIdentifier(
|
|
link?.parentPieceLinkId,
|
|
source?.parentPieceLinkId,
|
|
),
|
|
parentRequirementId: resolveIdentifier(
|
|
link?.parentRequirementId,
|
|
source?.parentRequirementId,
|
|
requirement?.parentRequirementId,
|
|
),
|
|
parentComponentRequirementId: resolveIdentifier(
|
|
link?.parentComponentRequirementId,
|
|
source?.parentComponentRequirementId,
|
|
requirement?.parentComponentRequirementId,
|
|
),
|
|
parentPieceRequirementId: resolveIdentifier(
|
|
link?.parentPieceRequirementId,
|
|
source?.parentPieceRequirementId,
|
|
requirement?.parentPieceRequirementId,
|
|
),
|
|
parentMachineComponentRequirementId: resolveIdentifier(
|
|
link?.parentMachineComponentRequirementId,
|
|
source?.parentMachineComponentRequirementId,
|
|
requirement?.parentMachineComponentRequirementId,
|
|
),
|
|
parentMachinePieceRequirementId: resolveIdentifier(
|
|
link?.parentMachinePieceRequirementId,
|
|
source?.parentMachinePieceRequirementId,
|
|
requirement?.parentMachinePieceRequirementId,
|
|
),
|
|
typeProductId: resolveIdentifier(
|
|
link?.typeProductId,
|
|
source?.typeProductId,
|
|
requirement?.typeProductId,
|
|
requirement?.typeProduct?.id,
|
|
),
|
|
}
|
|
}
|
|
|
|
const resetSkeletonRequirementSelections = () => {
|
|
Object.keys(componentRequirementSelections).forEach((key) => {
|
|
delete componentRequirementSelections[key]
|
|
})
|
|
Object.keys(pieceRequirementSelections).forEach((key) => {
|
|
delete pieceRequirementSelections[key]
|
|
})
|
|
Object.keys(productRequirementSelections).forEach((key) => {
|
|
delete productRequirementSelections[key]
|
|
})
|
|
}
|
|
|
|
const addComponentSelectionEntry = (requirement) => {
|
|
const entries = getComponentRequirementEntries(requirement.id)
|
|
const max = requirement.maxCount ?? null
|
|
if (max !== null && entries.length >= max) {
|
|
toast.showError(
|
|
`Vous ne pouvez pas ajouter plus de ${max} composant(s) pour ${requirement.label || requirement.typeComposant?.name || 'ce groupe'}`
|
|
)
|
|
return
|
|
}
|
|
componentRequirementSelections[requirement.id] = [
|
|
...entries,
|
|
createComponentSelectionEntry(requirement),
|
|
]
|
|
}
|
|
|
|
const removeComponentSelectionEntry = (requirementId, index) => {
|
|
const entries = getComponentRequirementEntries(requirementId)
|
|
componentRequirementSelections[requirementId] = entries.filter((_, i) => i !== index)
|
|
}
|
|
|
|
const setComponentRequirementType = (requirementId, index, value) => {
|
|
const entries = getComponentRequirementEntries(requirementId)
|
|
const entry = entries[index]
|
|
if (!entry) return
|
|
entry.typeComposantId = value || null
|
|
}
|
|
|
|
const setComponentRequirementConstructeur = (requirementId, index, value) => {
|
|
const entries = getComponentRequirementEntries(requirementId)
|
|
const entry = entries[index]
|
|
if (!entry) return
|
|
const ids = uniqueConstructeurIds(value)
|
|
entry.definition.constructeurIds = ids
|
|
entry.definition.constructeurId = ids[0] || null
|
|
}
|
|
|
|
const addPieceSelectionEntry = (requirement) => {
|
|
const entries = getPieceRequirementEntries(requirement.id)
|
|
const max = requirement.maxCount ?? null
|
|
if (max !== null && entries.length >= max) {
|
|
toast.showError(
|
|
`Vous ne pouvez pas ajouter plus de ${max} pièce(s) pour ${requirement.label || requirement.typePiece?.name || 'ce groupe'}`
|
|
)
|
|
return
|
|
}
|
|
pieceRequirementSelections[requirement.id] = [...entries, createPieceSelectionEntry(requirement)]
|
|
}
|
|
|
|
const removePieceSelectionEntry = (requirementId, index) => {
|
|
const entries = getPieceRequirementEntries(requirementId)
|
|
pieceRequirementSelections[requirementId] = entries.filter((_, i) => i !== index)
|
|
}
|
|
|
|
const setPieceRequirementType = (requirementId, index, value) => {
|
|
const entries = getPieceRequirementEntries(requirementId)
|
|
const entry = entries[index]
|
|
if (!entry) return
|
|
entry.typePieceId = value || null
|
|
}
|
|
|
|
const setPieceRequirementConstructeur = (requirementId, index, value) => {
|
|
const entries = getPieceRequirementEntries(requirementId)
|
|
const entry = entries[index]
|
|
if (!entry) return
|
|
const ids = uniqueConstructeurIds(value)
|
|
entry.definition.constructeurIds = ids
|
|
entry.definition.constructeurId = ids[0] || null
|
|
}
|
|
|
|
const addProductSelectionEntry = (requirement) => {
|
|
const entries = getProductRequirementEntries(requirement.id)
|
|
const max = requirement.maxCount ?? null
|
|
if (max !== null && entries.length >= max) {
|
|
toast.showError(
|
|
`Vous ne pouvez pas ajouter plus de ${max} produit(s) pour ${requirement.label || requirement.typeProduct?.name || 'ce groupe'}`,
|
|
)
|
|
return
|
|
}
|
|
productRequirementSelections[requirement.id] = [
|
|
...entries,
|
|
createProductSelectionEntry(requirement),
|
|
]
|
|
}
|
|
|
|
const removeProductSelectionEntry = (requirementId, index) => {
|
|
const entries = getProductRequirementEntries(requirementId)
|
|
productRequirementSelections[requirementId] = entries.filter((_, i) => i !== index)
|
|
}
|
|
|
|
const setProductRequirementProduct = (requirementId, index, productId) => {
|
|
const entries = getProductRequirementEntries(requirementId)
|
|
const entry = entries[index]
|
|
if (!entry) return
|
|
|
|
const normalizedProductId = productId || null
|
|
entry.productId = normalizedProductId
|
|
|
|
if (normalizedProductId) {
|
|
const product = findProductById(normalizedProductId)
|
|
entry.typeProductId =
|
|
product?.typeProductId ||
|
|
product?.typeProduct?.id ||
|
|
entry.typeProductId ||
|
|
null
|
|
}
|
|
}
|
|
|
|
const setProductRequirementType = (requirementId, index, value) => {
|
|
const entries = getProductRequirementEntries(requirementId)
|
|
const entry = entries[index]
|
|
if (!entry) return
|
|
entry.typeProductId = value || entry.typeProductId || null
|
|
}
|
|
|
|
const collectPiecesForSkeleton = () => {
|
|
const aggregated = []
|
|
machinePieces.value.forEach((piece) => {
|
|
aggregated.push(piece)
|
|
})
|
|
flattenedComponents.value.forEach((component) => {
|
|
;(component.pieces || []).forEach((piece) => {
|
|
aggregated.push(piece)
|
|
})
|
|
})
|
|
return aggregated
|
|
}
|
|
|
|
const initializeSkeletonRequirementSelections = async () => {
|
|
skeletonEditor.loading = true
|
|
try {
|
|
resetSkeletonRequirementSelections()
|
|
const type = machineType.value
|
|
if (!type) {
|
|
return
|
|
}
|
|
|
|
try {
|
|
await loadProducts()
|
|
} catch (error) {
|
|
console.error('Erreur lors du chargement des produits pour le squelette:', error)
|
|
}
|
|
|
|
;(type.componentRequirements || []).forEach((requirement) => {
|
|
const existingComponents = flattenedComponents.value.filter(
|
|
(component) => component.typeMachineComponentRequirementId === requirement.id,
|
|
)
|
|
const entries = existingComponents.map((component) =>
|
|
createComponentSelectionEntry(requirement, component),
|
|
)
|
|
|
|
const min = requirement.minCount ?? (requirement.required ? 1 : 0)
|
|
while (entries.length < min) {
|
|
entries.push(createComponentSelectionEntry(requirement))
|
|
}
|
|
|
|
if (entries.length) {
|
|
componentRequirementSelections[requirement.id] = entries
|
|
}
|
|
})
|
|
|
|
const allPieces = collectPiecesForSkeleton()
|
|
;(type.pieceRequirements || []).forEach((requirement) => {
|
|
const existingPieces = allPieces.filter(
|
|
(piece) => piece.typeMachinePieceRequirementId === requirement.id,
|
|
)
|
|
const entries = existingPieces.map((piece) =>
|
|
createPieceSelectionEntry(requirement, piece),
|
|
)
|
|
|
|
const min = requirement.minCount ?? (requirement.required ? 1 : 0)
|
|
while (entries.length < min) {
|
|
entries.push(createPieceSelectionEntry(requirement))
|
|
}
|
|
|
|
if (entries.length) {
|
|
pieceRequirementSelections[requirement.id] = entries
|
|
}
|
|
})
|
|
|
|
const existingProductLinks = Array.isArray(machineProductLinks.value)
|
|
? machineProductLinks.value
|
|
: Array.isArray(machine.value?.productLinks)
|
|
? machine.value.productLinks
|
|
: []
|
|
|
|
;(type.productRequirements || []).forEach((requirement) => {
|
|
const matches = existingProductLinks.filter((link) => {
|
|
const requirementId = resolveIdentifier(
|
|
link?.typeMachineProductRequirementId,
|
|
link?.requirementId,
|
|
)
|
|
return requirementId === requirement.id
|
|
})
|
|
|
|
const entries = matches.map((link) => createProductSelectionEntry(requirement, link))
|
|
|
|
const min = requirement.minCount ?? (requirement.required ? 1 : 0)
|
|
while (entries.length < min) {
|
|
entries.push(createProductSelectionEntry(requirement))
|
|
}
|
|
|
|
if (entries.length) {
|
|
productRequirementSelections[requirement.id] = entries
|
|
}
|
|
})
|
|
} finally {
|
|
skeletonEditor.loading = false
|
|
}
|
|
}
|
|
|
|
const openSkeletonEditor = async () => {
|
|
if (skeletonEditor.open) {
|
|
return
|
|
}
|
|
skeletonEditor.open = true
|
|
await initializeSkeletonRequirementSelections()
|
|
}
|
|
|
|
const closeSkeletonEditor = () => {
|
|
if (!skeletonEditor.open) {
|
|
return
|
|
}
|
|
if (skeletonEditor.submitting) {
|
|
return
|
|
}
|
|
skeletonEditor.open = false
|
|
skeletonEditor.loading = false
|
|
skeletonEditor.submitting = false
|
|
resetSkeletonRequirementSelections()
|
|
}
|
|
|
|
const changeMachineView = async (view) => {
|
|
if (view === activeMachineView.value) {
|
|
return
|
|
}
|
|
|
|
if (view === 'skeleton') {
|
|
if (!machineHasSkeletonRequirements.value) {
|
|
toast.showInfo('Aucun squelette configuré pour cette machine.')
|
|
return
|
|
}
|
|
|
|
activeMachineView.value = 'skeleton'
|
|
|
|
if (!skeletonEditor.open) {
|
|
try {
|
|
await openSkeletonEditor()
|
|
} catch (error) {
|
|
console.error('Impossible d\'ouvrir l\'éditeur de squelette:', error)
|
|
toast.showError('Impossible de charger les éléments du squelette.')
|
|
activeMachineView.value = 'details'
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
closeSkeletonEditor()
|
|
activeMachineView.value = 'details'
|
|
}
|
|
|
|
const computeSkeletonProductUsage = (type) => {
|
|
const usage = new Map()
|
|
|
|
const increment = (typeProductId) => {
|
|
if (!typeProductId) {
|
|
return
|
|
}
|
|
usage.set(typeProductId, (usage.get(typeProductId) ?? 0) + 1)
|
|
}
|
|
|
|
for (const requirement of type.componentRequirements || []) {
|
|
const entries = getComponentRequirementEntries(requirement.id)
|
|
entries.forEach((entry) => {
|
|
if (!entry?.composantId) {
|
|
return
|
|
}
|
|
const component = findComponentById(components.value, entry.composantId)
|
|
const typeProductId =
|
|
component?.product?.typeProductId ||
|
|
component?.product?.typeProduct?.id ||
|
|
null
|
|
increment(typeProductId)
|
|
})
|
|
}
|
|
|
|
for (const requirement of type.pieceRequirements || []) {
|
|
const entries = getPieceRequirementEntries(requirement.id)
|
|
entries.forEach((entry) => {
|
|
if (!entry?.pieceId) {
|
|
return
|
|
}
|
|
const piece = findPieceById(entry.pieceId)
|
|
const typeProductId =
|
|
piece?.product?.typeProductId ||
|
|
piece?.product?.typeProduct?.id ||
|
|
null
|
|
increment(typeProductId)
|
|
})
|
|
}
|
|
|
|
for (const requirement of type.productRequirements || []) {
|
|
const entries = getProductRequirementEntries(requirement.id)
|
|
entries.forEach((entry) => {
|
|
if (!entry?.productId) {
|
|
return
|
|
}
|
|
const product = findProductById(entry.productId)
|
|
const typeProductId =
|
|
product?.typeProductId ||
|
|
product?.typeProduct?.id ||
|
|
entry?.typeProductId ||
|
|
requirement?.typeProductId ||
|
|
requirement?.typeProduct?.id ||
|
|
null
|
|
increment(typeProductId)
|
|
})
|
|
}
|
|
|
|
return usage
|
|
}
|
|
|
|
const validateSkeletonSelections = (type) => {
|
|
const errors = []
|
|
const componentLinksPayload = []
|
|
const pieceLinksPayload = []
|
|
const productLinksPayload = []
|
|
|
|
for (const requirement of type.componentRequirements || []) {
|
|
const entries = getComponentRequirementEntries(requirement.id)
|
|
const min = requirement.minCount ?? (requirement.required ? 1 : 0)
|
|
const max = requirement.maxCount ?? null
|
|
|
|
if (entries.length < min) {
|
|
errors.push(
|
|
`Le groupe "${requirement.label || requirement.typeComposant?.name || 'Composants'}" nécessite au moins ${min} élément(s).`,
|
|
)
|
|
}
|
|
|
|
if (max !== null && entries.length > max) {
|
|
errors.push(
|
|
`Le groupe "${requirement.label || requirement.typeComposant?.name || 'Composants'}" ne peut dépasser ${max} élément(s).`,
|
|
)
|
|
}
|
|
|
|
entries.forEach((entry) => {
|
|
const resolvedTypeId = entry.typeComposantId || requirement.typeComposantId || null
|
|
if (!resolvedTypeId) {
|
|
errors.push(
|
|
`Le groupe "${requirement.label || requirement.typeComposant?.name || 'Composants'}" nécessite un type de composant.`,
|
|
)
|
|
return
|
|
}
|
|
|
|
const payload = {
|
|
requirementId: requirement.id,
|
|
typeComposantId: resolvedTypeId,
|
|
}
|
|
|
|
if (entry.linkId) {
|
|
payload.id = entry.linkId
|
|
payload.linkId = entry.linkId
|
|
}
|
|
|
|
if (entry.composantId) {
|
|
payload.composantId = entry.composantId
|
|
}
|
|
|
|
const overrides = sanitizeDefinitionOverrides(entry.definition)
|
|
if (overrides) {
|
|
payload.overrides = overrides
|
|
}
|
|
|
|
Object.assign(
|
|
payload,
|
|
extractParentLinkIdentifiers(requirement),
|
|
extractParentLinkIdentifiers(entry),
|
|
)
|
|
|
|
componentLinksPayload.push(payload)
|
|
})
|
|
}
|
|
|
|
for (const requirement of type.pieceRequirements || []) {
|
|
const entries = getPieceRequirementEntries(requirement.id)
|
|
const min = requirement.minCount ?? (requirement.required ? 1 : 0)
|
|
const max = requirement.maxCount ?? null
|
|
|
|
if (entries.length < min) {
|
|
errors.push(
|
|
`Le groupe "${requirement.label || requirement.typePiece?.name || 'Pièces'}" nécessite au moins ${min} élément(s).`,
|
|
)
|
|
}
|
|
|
|
if (max !== null && entries.length > max) {
|
|
errors.push(
|
|
`Le groupe "${requirement.label || requirement.typePiece?.name || 'Pièces'}" ne peut dépasser ${max} élément(s).`,
|
|
)
|
|
}
|
|
|
|
entries.forEach((entry) => {
|
|
const resolvedTypeId = entry.typePieceId || requirement.typePieceId || null
|
|
if (!resolvedTypeId) {
|
|
errors.push(
|
|
`Le groupe "${requirement.label || requirement.typePiece?.name || 'Pièces'}" nécessite un type de pièce.`,
|
|
)
|
|
return
|
|
}
|
|
|
|
const payload = {
|
|
requirementId: requirement.id,
|
|
typePieceId: resolvedTypeId,
|
|
}
|
|
|
|
if (entry.linkId) {
|
|
payload.id = entry.linkId
|
|
payload.linkId = entry.linkId
|
|
}
|
|
|
|
if (entry.pieceId) {
|
|
payload.pieceId = entry.pieceId
|
|
}
|
|
|
|
if (entry.composantId) {
|
|
payload.composantId = entry.composantId
|
|
}
|
|
|
|
const overrides = sanitizeDefinitionOverrides(entry.definition)
|
|
if (overrides) {
|
|
payload.overrides = overrides
|
|
}
|
|
|
|
Object.assign(
|
|
payload,
|
|
extractParentLinkIdentifiers(requirement),
|
|
extractParentLinkIdentifiers(entry),
|
|
)
|
|
|
|
pieceLinksPayload.push(payload)
|
|
})
|
|
}
|
|
|
|
const productUsage = computeSkeletonProductUsage(type)
|
|
|
|
for (const requirement of type.productRequirements || []) {
|
|
const entries = getProductRequirementEntries(requirement.id)
|
|
const max = requirement.maxCount ?? null
|
|
|
|
if (max !== null && entries.length > max) {
|
|
errors.push(
|
|
`Le groupe "${requirement.label || requirement.typeProduct?.name || 'Produits'}" ne peut dépasser ${max} sélection(s) directe(s).`,
|
|
)
|
|
}
|
|
|
|
const typeProductId =
|
|
requirement.typeProductId ||
|
|
requirement.typeProduct?.id ||
|
|
null
|
|
|
|
const count = typeProductId ? productUsage.get(typeProductId) ?? 0 : 0
|
|
const min = requirement.minCount ?? (requirement.required ? 1 : 0)
|
|
|
|
if (count < min) {
|
|
errors.push(
|
|
`Le groupe "${requirement.label || requirement.typeProduct?.name || 'Produits'}" nécessite au moins ${min} sélection(s).`,
|
|
)
|
|
}
|
|
|
|
if (max !== null && count > max) {
|
|
errors.push(
|
|
`Le groupe "${requirement.label || requirement.typeProduct?.name || 'Produits'}" ne peut dépasser ${max} sélection(s).`,
|
|
)
|
|
}
|
|
|
|
entries.forEach((entry) => {
|
|
if (!entry.productId) {
|
|
errors.push(
|
|
`Sélectionner un produit pour "${requirement.label || requirement.typeProduct?.name || 'Produits'}".`,
|
|
)
|
|
return
|
|
}
|
|
|
|
const product = findProductById(entry.productId)
|
|
if (!product) {
|
|
errors.push(`Le produit sélectionné est introuvable (ID: ${entry.productId}).`)
|
|
return
|
|
}
|
|
|
|
const productTypeId =
|
|
product.typeProductId ||
|
|
product.typeProduct?.id ||
|
|
entry.typeProductId ||
|
|
null
|
|
|
|
if (
|
|
typeProductId &&
|
|
productTypeId &&
|
|
productTypeId !== typeProductId
|
|
) {
|
|
errors.push(
|
|
`Le produit "${product.name || product.reference || product.id}" n'appartient pas à la catégorie attendue.`,
|
|
)
|
|
return
|
|
}
|
|
|
|
const payload = {
|
|
requirementId: requirement.id,
|
|
productId: entry.productId,
|
|
}
|
|
|
|
if (entry.linkId) {
|
|
payload.id = entry.linkId
|
|
payload.linkId = entry.linkId
|
|
}
|
|
|
|
if (entry.typeProductId) {
|
|
payload.typeProductId = entry.typeProductId
|
|
}
|
|
|
|
Object.assign(
|
|
payload,
|
|
extractParentLinkIdentifiers(requirement),
|
|
extractParentLinkIdentifiers(entry),
|
|
)
|
|
|
|
productLinksPayload.push(payload)
|
|
})
|
|
}
|
|
|
|
if (errors.length > 0) {
|
|
return { valid: false, error: errors[0] }
|
|
}
|
|
|
|
return {
|
|
valid: true,
|
|
componentLinks: componentLinksPayload,
|
|
pieceLinks: pieceLinksPayload,
|
|
productLinks: productLinksPayload,
|
|
}
|
|
}
|
|
|
|
const applySkeletonReconfigurationResult = async (data) => {
|
|
if (!data) return
|
|
|
|
const updatedMachine = data.machine || data
|
|
if (updatedMachine) {
|
|
machine.value = {
|
|
...machine.value,
|
|
...updatedMachine,
|
|
documents: updatedMachine.documents || machine.value?.documents || [],
|
|
}
|
|
initMachineFields()
|
|
machineDocumentsLoaded.value = !!(machine.value.documents?.length)
|
|
}
|
|
|
|
const linksApplied = applyMachineLinks(data) || applyMachineLinks(updatedMachine)
|
|
if (linksApplied) {
|
|
if (machine.value) {
|
|
machine.value.componentLinks = machineComponentLinks.value
|
|
machine.value.pieceLinks = machinePieceLinks.value
|
|
machine.value.productLinks = machineProductLinks.value
|
|
}
|
|
collapseAllComponents()
|
|
return
|
|
}
|
|
|
|
const newComponents = data.components ?? updatedMachine?.components ?? null
|
|
if (Array.isArray(newComponents)) {
|
|
components.value = transformComponentCustomFields(newComponents)
|
|
collapseAllComponents()
|
|
}
|
|
|
|
const newPieces = data.pieces ?? updatedMachine?.pieces ?? null
|
|
if (Array.isArray(newPieces)) {
|
|
pieces.value = transformCustomFields(newPieces)
|
|
}
|
|
|
|
const productLinks = resolveLinkArray(data, ['productLinks', 'machineProductLinks'])
|
|
?? resolveLinkArray(updatedMachine, ['productLinks', 'machineProductLinks'])
|
|
if (Array.isArray(productLinks)) {
|
|
machineProductLinks.value = productLinks
|
|
if (machine.value) {
|
|
machine.value.productLinks = productLinks
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
const saveSkeletonConfiguration = async () => {
|
|
if (!machine.value?.id) {
|
|
return
|
|
}
|
|
|
|
const type = machineType.value
|
|
let payload = { componentLinks: [], pieceLinks: [], productLinks: [] }
|
|
|
|
if (type && machineHasSkeletonRequirements.value) {
|
|
const validation = validateSkeletonSelections(type)
|
|
if (!validation.valid) {
|
|
toast.showError(validation.error)
|
|
return
|
|
}
|
|
payload = {
|
|
componentLinks: validation.componentLinks,
|
|
pieceLinks: validation.pieceLinks,
|
|
productLinks: validation.productLinks,
|
|
}
|
|
}
|
|
|
|
skeletonEditor.submitting = true
|
|
try {
|
|
const result = await reconfigureMachineSkeleton(machine.value.id, payload)
|
|
if (result.success) {
|
|
await applySkeletonReconfigurationResult(result.data)
|
|
await changeMachineView('details')
|
|
} else if (result.error) {
|
|
toast.showError(result.error)
|
|
}
|
|
} catch (error) {
|
|
console.error('Erreur lors de la reconfiguration du squelette de la machine:', error)
|
|
toast.showError('Erreur lors de la mise à jour des éléments du squelette')
|
|
} finally {
|
|
skeletonEditor.submitting = false
|
|
skeletonEditor.loading = false
|
|
}
|
|
}
|
|
|
|
const handleMachineConstructeurChange = async (value) => {
|
|
machineConstructeurIds.value = uniqueConstructeurIds(value)
|
|
await updateMachineInfo()
|
|
}
|
|
|
|
// Mode d'édition
|
|
const isEditMode = ref(false)
|
|
const debug = ref(false) // Ajout de debug pour afficher les infos de debug
|
|
|
|
const machineViewTitle = computed(() => {
|
|
if (isSkeletonView.value) {
|
|
return 'Squelette de la machine'
|
|
}
|
|
return isEditMode.value ? 'Modification de la machine' : 'Détails de la machine'
|
|
})
|
|
|
|
// Gestion du pliage global des composants
|
|
const componentsCollapsed = ref(true)
|
|
const collapseToggleToken = ref(0)
|
|
|
|
const toggleAllComponents = () => {
|
|
componentsCollapsed.value = !componentsCollapsed.value
|
|
collapseToggleToken.value += 1
|
|
}
|
|
|
|
const collapseAllComponents = () => {
|
|
componentsCollapsed.value = true
|
|
collapseToggleToken.value += 1
|
|
}
|
|
|
|
// Méthodes pour initialiser les champs
|
|
const initMachineFields = () => {
|
|
if (machine.value) {
|
|
machineName.value = machine.value.name || ''
|
|
machineReference.value = machine.value.reference || ''
|
|
machineConstructeurIds.value = uniqueConstructeurIds(
|
|
machine.value.constructeurIds,
|
|
machine.value.constructeurs,
|
|
machine.value.constructeur,
|
|
)
|
|
}
|
|
}
|
|
|
|
// Fonctions pour générer des IDs uniques
|
|
const getMachineFieldId = (fieldName) => {
|
|
return machine.value ? `machine-${fieldName}-${machine.value.id}` : `machine-${fieldName}`
|
|
}
|
|
|
|
// Computed
|
|
const machinePieces = computed(() => {
|
|
const filteredPieces = pieces.value.filter((piece) => {
|
|
const parentLinkId = resolveIdentifier(
|
|
piece.parentComponentLinkId,
|
|
piece.machinePieceLink?.parentComponentLinkId,
|
|
piece.parentLinkId,
|
|
)
|
|
if (parentLinkId) {
|
|
return false
|
|
}
|
|
return !piece.composantId
|
|
})
|
|
return filteredPieces
|
|
})
|
|
|
|
const machineDocumentsList = computed(() => machine.value?.documents || [])
|
|
const documentIcon = (doc) => getFileIcon({ name: doc.filename || doc.name, mime: doc.mimeType })
|
|
|
|
const flattenComponents = (list = []) => {
|
|
const result = []
|
|
const traverse = (items) => {
|
|
items.forEach((item) => {
|
|
result.push(item)
|
|
if (item.subComponents && item.subComponents.length) {
|
|
traverse(item.subComponents)
|
|
}
|
|
})
|
|
}
|
|
traverse(list)
|
|
return result
|
|
}
|
|
|
|
const flattenedComponents = computed(() => flattenComponents(components.value))
|
|
|
|
const collectConstructeurs = (...sources) => {
|
|
const ids = uniqueConstructeurIds(...sources)
|
|
if (!ids.length) {
|
|
return []
|
|
}
|
|
|
|
const pools = sources
|
|
.flatMap((source) => {
|
|
if (Array.isArray(source)) {
|
|
return [source]
|
|
}
|
|
if (source && typeof source === 'object' && source.id) {
|
|
return [[source]]
|
|
}
|
|
return []
|
|
})
|
|
.filter(Boolean)
|
|
|
|
return resolveConstructeurs(ids, ...pools)
|
|
}
|
|
|
|
const componentRequirementGroups = computed(() => {
|
|
const requirements = machine.value?.typeMachine?.componentRequirements || []
|
|
if (!requirements.length) return []
|
|
|
|
const groups = requirements.map((requirement) => ({
|
|
requirement,
|
|
components: [],
|
|
}))
|
|
|
|
const map = new Map(groups.map((group) => [group.requirement.id, group]))
|
|
|
|
flattenedComponents.value.forEach((component) => {
|
|
const reqId = component.typeMachineComponentRequirementId
|
|
if (reqId && map.has(reqId)) {
|
|
map.get(reqId).components.push({
|
|
...component,
|
|
__productDisplay: getProductDisplay(component),
|
|
})
|
|
}
|
|
})
|
|
|
|
return groups
|
|
})
|
|
|
|
const pieceRequirementGroups = computed(() => {
|
|
const requirements = machine.value?.typeMachine?.pieceRequirements || []
|
|
if (!requirements.length) return []
|
|
|
|
const groups = requirements.map((requirement) => ({
|
|
requirement,
|
|
pieces: [],
|
|
}))
|
|
|
|
const map = new Map(groups.map((group) => [group.requirement.id, group]))
|
|
|
|
const collectPieces = () => {
|
|
const collected = []
|
|
|
|
// Pièces rattachées à la machine directement
|
|
machinePieces.value.forEach((piece) => {
|
|
collected.push({
|
|
...piece,
|
|
constructeurs: piece.constructeurs || [],
|
|
parentComponentName: null,
|
|
__productDisplay: getProductDisplay(piece),
|
|
})
|
|
})
|
|
|
|
// Pièces rattachées aux composants
|
|
flattenedComponents.value.forEach((component) => {
|
|
if (component.pieces && component.pieces.length) {
|
|
component.pieces.forEach((piece) => {
|
|
collected.push({
|
|
...piece,
|
|
constructeurs: piece.constructeurs || [],
|
|
parentComponentName: component.name,
|
|
__productDisplay: getProductDisplay(piece),
|
|
})
|
|
})
|
|
}
|
|
})
|
|
|
|
return collected
|
|
}
|
|
|
|
collectPieces().forEach((piece) => {
|
|
const reqId = piece.typeMachinePieceRequirementId
|
|
if (reqId && map.has(reqId)) {
|
|
map.get(reqId).pieces.push(piece)
|
|
}
|
|
})
|
|
|
|
return groups
|
|
})
|
|
|
|
const productRequirementGroups = computed(() => {
|
|
const requirements = machine.value?.typeMachine?.productRequirements || []
|
|
if (!requirements.length) return []
|
|
|
|
const componentAggregates = flattenedComponents.value || []
|
|
const pieceAggregates = collectPiecesForSkeleton()
|
|
const links = Array.isArray(machineProductLinks.value) ? machineProductLinks.value : []
|
|
|
|
return requirements.map((requirement) => {
|
|
const typeProductId =
|
|
requirement.typeProductId ||
|
|
requirement.typeProduct?.id ||
|
|
null
|
|
|
|
const directProducts = links
|
|
.filter((link) => {
|
|
const requirementId = resolveIdentifier(
|
|
link?.typeMachineProductRequirementId,
|
|
link?.requirementId,
|
|
)
|
|
return requirementId === requirement.id
|
|
})
|
|
.map((link) => {
|
|
const productId = resolveIdentifier(link?.productId, link?.product?.id)
|
|
const product =
|
|
productId ? findProductById(productId) : link?.product ?? null
|
|
|
|
const supplierLabel = Array.isArray(product?.constructeurs)
|
|
? product.constructeurs.map((constructeur) => constructeur?.name).filter(Boolean).join(', ')
|
|
: link?.constructeursLabel || null
|
|
|
|
const priceValue =
|
|
product?.supplierPrice ??
|
|
link?.supplierPrice ??
|
|
null
|
|
|
|
let priceLabel = null
|
|
if (priceValue !== undefined && priceValue !== null) {
|
|
const numericPrice = Number(priceValue)
|
|
if (!Number.isNaN(numericPrice)) {
|
|
priceLabel = `${numericPrice.toFixed(2)} €`
|
|
}
|
|
}
|
|
|
|
return {
|
|
id: productId || link?.id || null,
|
|
name: product?.name || link?.productName || productId || 'Produit',
|
|
reference: product?.reference || link?.reference || null,
|
|
supplierLabel,
|
|
priceLabel,
|
|
}
|
|
})
|
|
|
|
let componentCount = 0
|
|
componentAggregates.forEach((component) => {
|
|
const componentTypeProductId =
|
|
component?.product?.typeProductId ||
|
|
component?.product?.typeProduct?.id ||
|
|
null
|
|
if (typeProductId && componentTypeProductId === typeProductId) {
|
|
componentCount += 1
|
|
}
|
|
})
|
|
|
|
let pieceCount = 0
|
|
pieceAggregates.forEach((piece) => {
|
|
const pieceTypeProductId =
|
|
piece?.product?.typeProductId ||
|
|
piece?.product?.typeProduct?.id ||
|
|
null
|
|
if (typeProductId && pieceTypeProductId === typeProductId) {
|
|
pieceCount += 1
|
|
}
|
|
})
|
|
|
|
const totalCount = directProducts.length + componentCount + pieceCount
|
|
|
|
return {
|
|
requirement,
|
|
directProducts,
|
|
componentCount,
|
|
pieceCount,
|
|
totalCount,
|
|
}
|
|
})
|
|
})
|
|
|
|
const machineDirectProducts = computed(() => {
|
|
return productRequirementGroups.value.flatMap((group) =>
|
|
(group.directProducts || []).map((product) => ({
|
|
...product,
|
|
groupLabel:
|
|
group.requirement.label ||
|
|
group.requirement.typeProduct?.name ||
|
|
'Produit requis',
|
|
})),
|
|
)
|
|
})
|
|
|
|
const findComponentById = (items, id) => {
|
|
for (const item of items || []) {
|
|
if (item.id === id) return item
|
|
const found = findComponentById(item.subComponents, id)
|
|
if (found) return found
|
|
}
|
|
return null
|
|
}
|
|
|
|
const findPieceById = (pieceId) => {
|
|
const direct = pieces.value.find((piece) => piece.id === pieceId)
|
|
if (direct) return direct
|
|
|
|
const searchInComponents = (items) => {
|
|
for (const item of items || []) {
|
|
const match = (item.pieces || []).find((piece) => piece.id === pieceId)
|
|
if (match) return match
|
|
const nested = searchInComponents(item.subComponents)
|
|
if (nested) return nested
|
|
}
|
|
return null
|
|
}
|
|
|
|
return searchInComponents(components.value)
|
|
}
|
|
|
|
const refreshMachineDocuments = async () => {
|
|
if (!machine.value?.id) return
|
|
const result = await loadDocumentsByMachine(machine.value.id, { updateStore: false })
|
|
if (result.success && machine.value) {
|
|
machine.value.documents = result.data || []
|
|
machineDocumentsLoaded.value = true
|
|
}
|
|
}
|
|
|
|
const handleMachineFilesAdded = async (files) => {
|
|
if (!files.length || !machine.value?.id) return
|
|
machineDocumentsUploading.value = true
|
|
try {
|
|
const result = await uploadDocuments(
|
|
{
|
|
files,
|
|
context: { machineId: machine.value.id }
|
|
},
|
|
{ updateStore: false }
|
|
)
|
|
|
|
if (result.success && machine.value) {
|
|
const newDocs = result.data || []
|
|
machine.value.documents = [...newDocs, ...(machine.value.documents || [])]
|
|
machineDocumentFiles.value = []
|
|
}
|
|
} finally {
|
|
machineDocumentsUploading.value = false
|
|
}
|
|
}
|
|
|
|
const removeMachineDocument = async (documentId) => {
|
|
if (!documentId) return
|
|
const result = await deleteDocument(documentId, { updateStore: false })
|
|
if (result.success && machine.value) {
|
|
machine.value.documents = (machine.value.documents || []).filter(doc => doc.id !== documentId)
|
|
}
|
|
}
|
|
|
|
const downloadDocument = (doc) => {
|
|
if (!doc?.path) return
|
|
|
|
if (doc.path.startsWith('data:')) {
|
|
const link = document.createElement('a')
|
|
link.href = doc.path
|
|
link.download = doc.filename || doc.name || 'document'
|
|
link.click()
|
|
return
|
|
}
|
|
|
|
window.open(doc.path, '_blank')
|
|
}
|
|
|
|
const openPreview = (doc) => {
|
|
if (!canPreviewDocument(doc)) return
|
|
previewDocument.value = doc
|
|
previewVisible.value = true
|
|
}
|
|
|
|
const closePreview = () => {
|
|
previewVisible.value = false
|
|
previewDocument.value = null
|
|
}
|
|
|
|
const ensurePrintSelectionEntries = () => {
|
|
printSelection.machine.info ??= true
|
|
printSelection.machine.customFields ??= true
|
|
printSelection.machine.documents ??= true
|
|
|
|
const ensureComponent = (component) => {
|
|
if (component?.id !== undefined && printSelection.components[component.id] === undefined) {
|
|
printSelection.components[component.id] = true
|
|
}
|
|
;(component.pieces || []).forEach((piece) => {
|
|
if (piece?.id !== undefined && printSelection.pieces[piece.id] === undefined) {
|
|
printSelection.pieces[piece.id] = true
|
|
}
|
|
})
|
|
;(component.subComponents || []).forEach(ensureComponent)
|
|
}
|
|
|
|
components.value.forEach(ensureComponent)
|
|
machinePieces.value.forEach((piece) => {
|
|
if (piece?.id !== undefined && printSelection.pieces[piece.id] === undefined) {
|
|
printSelection.pieces[piece.id] = true
|
|
}
|
|
})
|
|
}
|
|
|
|
const setAllPrintSelection = (value) => {
|
|
ensurePrintSelectionEntries()
|
|
printSelection.machine.info = value
|
|
printSelection.machine.customFields = value
|
|
printSelection.machine.documents = value
|
|
Object.keys(printSelection.components).forEach((key) => {
|
|
printSelection.components[key] = value
|
|
})
|
|
Object.keys(printSelection.pieces).forEach((key) => {
|
|
printSelection.pieces[key] = value
|
|
})
|
|
}
|
|
|
|
const openPrintModal = () => {
|
|
ensurePrintSelectionEntries()
|
|
printModalOpen.value = true
|
|
}
|
|
|
|
const closePrintModal = () => {
|
|
printModalOpen.value = false
|
|
}
|
|
|
|
const handlePrintConfirm = async () => {
|
|
closePrintModal()
|
|
await nextTick()
|
|
printMachine(printSelection)
|
|
}
|
|
|
|
|
|
const printMachine = (currentSelection = printSelection) => {
|
|
if (typeof window === 'undefined') return
|
|
|
|
const context = buildMachinePrintContext({
|
|
machine: machine.value,
|
|
machineName: machineName.value,
|
|
machineReference: machineReference.value,
|
|
machinePieces: machinePieces.value,
|
|
components: components.value,
|
|
selection: currentSelection,
|
|
})
|
|
const styles = Array.from(document.querySelectorAll('link[rel="stylesheet"], style'))
|
|
.map(node => node.outerHTML)
|
|
.join('')
|
|
|
|
const htmlContent = buildMachinePrintHtml(context, styles)
|
|
|
|
const iframe = document.createElement('iframe')
|
|
iframe.style.position = 'fixed'
|
|
iframe.style.right = '0'
|
|
iframe.style.bottom = '0'
|
|
iframe.style.width = '0'
|
|
iframe.style.height = '0'
|
|
iframe.style.border = '0'
|
|
iframe.setAttribute('title', 'print-frame')
|
|
document.body.appendChild(iframe)
|
|
|
|
const iframeWindow = iframe.contentWindow
|
|
const iframeDocument = iframe.contentDocument || iframeWindow?.document
|
|
if (!iframeDocument || !iframeWindow) {
|
|
iframe.remove()
|
|
return
|
|
}
|
|
|
|
iframeDocument.open()
|
|
iframeDocument.write(htmlContent)
|
|
iframeDocument.close()
|
|
|
|
const triggerPrint = () => {
|
|
iframeWindow.focus()
|
|
iframeWindow.print()
|
|
setTimeout(() => {
|
|
iframe.remove()
|
|
}, 1000)
|
|
}
|
|
|
|
if (iframeDocument.readyState === 'complete') {
|
|
setTimeout(triggerPrint, 50)
|
|
} else {
|
|
iframe.onload = () => setTimeout(triggerPrint, 50)
|
|
}
|
|
}
|
|
|
|
const formatSize = (size) => {
|
|
if (size === undefined || size === null) return '—'
|
|
if (size === 0) return '0 B'
|
|
const units = ['B', 'KB', 'MB', 'GB']
|
|
const index = Math.min(units.length - 1, Math.floor(Math.log(size) / Math.log(1024)))
|
|
const formatted = size / Math.pow(1024, 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) => {
|
|
if (!field) {
|
|
return 'Non défini'
|
|
}
|
|
|
|
const value = field.value ?? field.defaultValue ?? ''
|
|
if (value === '' || value === null || value === undefined) {
|
|
return 'Non défini'
|
|
}
|
|
|
|
if (field.type === 'boolean') {
|
|
const normalized = String(value).toLowerCase()
|
|
if (normalized === 'true' || normalized === '1') return 'Oui'
|
|
if (normalized === 'false' || normalized === '0') return 'Non'
|
|
}
|
|
|
|
return String(value)
|
|
}
|
|
|
|
const shouldDisplayCustomField = (field) => {
|
|
if (!field) {
|
|
return false
|
|
}
|
|
|
|
if (field.readOnly) {
|
|
return true
|
|
}
|
|
|
|
if (field.type === 'boolean') {
|
|
return field.value !== undefined && field.value !== null
|
|
}
|
|
|
|
const value = field.value
|
|
if (value === null || value === undefined) {
|
|
return false
|
|
}
|
|
|
|
if (typeof value === 'string') {
|
|
return value.trim().length > 0
|
|
}
|
|
|
|
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 seen = new Set()
|
|
return fields
|
|
.slice()
|
|
.sort((a, b) => {
|
|
const left = typeof a?.orderIndex === 'number' ? a.orderIndex : 0
|
|
const right = typeof b?.orderIndex === 'number' ? b.orderIndex : 0
|
|
return left - right
|
|
})
|
|
.filter(shouldDisplayCustomField)
|
|
.filter((field) => {
|
|
const key = field.customFieldId || field.id || field.name
|
|
if (!key) {
|
|
return true
|
|
}
|
|
if (seen.has(key)) {
|
|
return false
|
|
}
|
|
seen.add(key)
|
|
return true
|
|
})
|
|
.map((field, index) => ({
|
|
key: field.customFieldId || field.id || field.name || `custom-field-${index}`,
|
|
label: field.name || 'Champ',
|
|
value: formatCustomFieldValue(field),
|
|
}))
|
|
}
|
|
|
|
const extractDefinitionName = (definition = {}) => {
|
|
if (typeof definition?.name === 'string' && definition.name.trim()) {
|
|
return definition.name.trim()
|
|
}
|
|
if (typeof definition?.key === 'string' && definition.key.trim()) {
|
|
return definition.key.trim()
|
|
}
|
|
if (typeof definition?.label === 'string' && definition.label.trim()) {
|
|
return definition.label.trim()
|
|
}
|
|
return ''
|
|
}
|
|
|
|
const extractDefinitionType = (definition = {}, fallback = 'text') => {
|
|
const allowed = ['text', 'number', 'select', 'boolean', 'date']
|
|
const rawType =
|
|
typeof definition?.type === 'string'
|
|
? definition.type
|
|
: typeof definition?.value?.type === 'string'
|
|
? definition.value.type
|
|
: typeof fallback === 'string'
|
|
? fallback
|
|
: 'text'
|
|
const normalized = rawType.toLowerCase()
|
|
return allowed.includes(normalized) ? normalized : 'text'
|
|
}
|
|
|
|
const extractDefinitionRequired = (definition = {}, fallback = false) => {
|
|
if (typeof definition?.required === 'boolean') {
|
|
return definition.required
|
|
}
|
|
const nested = definition?.value?.required
|
|
if (typeof nested === 'boolean') {
|
|
return nested
|
|
}
|
|
if (typeof nested === 'string') {
|
|
const normalized = nested.toLowerCase()
|
|
if (normalized === 'true' || normalized === '1') {
|
|
return true
|
|
}
|
|
if (normalized === 'false' || normalized === '0') {
|
|
return false
|
|
}
|
|
}
|
|
return !!fallback
|
|
}
|
|
|
|
const extractOptionList = (input) => {
|
|
if (!Array.isArray(input)) {
|
|
return undefined
|
|
}
|
|
const mapped = input
|
|
.map((option) => {
|
|
if (option === null || option === undefined) {
|
|
return ''
|
|
}
|
|
if (typeof option === 'string') {
|
|
return option.trim()
|
|
}
|
|
if (typeof option === 'object') {
|
|
const record = option || {}
|
|
const keys = ['value', 'label', 'name']
|
|
for (const key of keys) {
|
|
const candidate = record[key]
|
|
if (typeof candidate === 'string' && candidate.trim().length > 0) {
|
|
return candidate.trim()
|
|
}
|
|
}
|
|
}
|
|
const fallback = String(option).trim()
|
|
return fallback === '[object Object]' ? '' : fallback
|
|
})
|
|
.filter((option) => option.length > 0)
|
|
return mapped.length ? mapped : undefined
|
|
}
|
|
|
|
const extractDefinitionOptions = (definition = {}) => {
|
|
const sources = [definition?.options, definition?.value?.options, definition?.value?.choices]
|
|
for (const source of sources) {
|
|
const list = extractOptionList(source)
|
|
if (list) {
|
|
return list
|
|
}
|
|
}
|
|
return []
|
|
}
|
|
|
|
const extractDefinitionDefaultValue = (definition = {}) => {
|
|
const candidates = [
|
|
definition?.defaultValue,
|
|
definition?.value?.defaultValue,
|
|
definition?.value?.value,
|
|
definition?.value,
|
|
definition?.default,
|
|
]
|
|
for (const candidate of candidates) {
|
|
if (candidate === undefined || candidate === null || candidate === '') {
|
|
continue
|
|
}
|
|
if (typeof candidate === 'object') {
|
|
if (candidate === null) {
|
|
continue
|
|
}
|
|
if (candidate && typeof candidate === 'object') {
|
|
const nestedDefault =
|
|
candidate.defaultValue !== undefined && candidate.defaultValue !== null
|
|
? candidate.defaultValue
|
|
: candidate.value
|
|
if (nestedDefault !== undefined && nestedDefault !== null && nestedDefault !== '') {
|
|
return nestedDefault
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
return candidate
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
const coerceValueForType = (type, rawValue) => {
|
|
if (rawValue === undefined || rawValue === null || rawValue === '') {
|
|
return ''
|
|
}
|
|
if (type === 'boolean') {
|
|
const normalized = String(rawValue).toLowerCase()
|
|
if (normalized === 'true' || normalized === '1') {
|
|
return 'true'
|
|
}
|
|
if (normalized === 'false' || normalized === '0') {
|
|
return 'false'
|
|
}
|
|
return ''
|
|
}
|
|
return String(rawValue)
|
|
}
|
|
|
|
const normalizeExistingCustomFieldDefinitions = (fields) => {
|
|
if (!Array.isArray(fields)) {
|
|
return []
|
|
}
|
|
return fields
|
|
.map((field, index) => normalizeCustomFieldDefinitionEntry(field, index))
|
|
.filter((definition) => definition !== null)
|
|
.sort((a, b) => (a.orderIndex ?? 0) - (b.orderIndex ?? 0))
|
|
}
|
|
|
|
const normalizeCustomFieldDefinitionEntry = (definition = {}, fallbackIndex = 0) => {
|
|
const name = extractDefinitionName(definition)
|
|
if (!name) {
|
|
return null
|
|
}
|
|
const type = extractDefinitionType(definition)
|
|
const required = extractDefinitionRequired(definition)
|
|
const options = extractDefinitionOptions(definition)
|
|
const defaultValue = extractDefinitionDefaultValue(definition)
|
|
const id = typeof definition?.id === 'string' ? definition.id : undefined
|
|
const customFieldId = typeof definition?.customFieldId === 'string' ? definition.customFieldId : id
|
|
const orderIndex = typeof definition?.orderIndex === 'number' ? definition.orderIndex : fallbackIndex
|
|
return {
|
|
id,
|
|
customFieldId,
|
|
name,
|
|
type,
|
|
required,
|
|
options,
|
|
defaultValue,
|
|
readOnly: !!definition?.readOnly,
|
|
orderIndex,
|
|
}
|
|
}
|
|
|
|
const getStructureCustomFields = (structure) => {
|
|
if (!structure || typeof structure !== 'object') {
|
|
return []
|
|
}
|
|
const normalized = normalizeStructureForEditor(structure)
|
|
return Array.isArray(normalized.customFields) ? normalized.customFields : []
|
|
}
|
|
|
|
// Transform custom field values to custom fields format
|
|
const normalizeCustomFieldValueEntry = (entry = {}) => {
|
|
if (!entry || typeof entry !== 'object') {
|
|
return null
|
|
}
|
|
|
|
const normalizedDefinition = normalizeCustomFieldDefinitionEntry(entry)
|
|
if (!normalizedDefinition) {
|
|
return null
|
|
}
|
|
|
|
const value = coerceValueForType(
|
|
normalizedDefinition.type,
|
|
entry?.value ?? entry?.defaultValue ?? normalizedDefinition.defaultValue ?? '',
|
|
)
|
|
|
|
return {
|
|
id: entry?.customFieldValueId ?? entry?.id ?? null,
|
|
customFieldId:
|
|
entry?.customFieldId
|
|
?? normalizedDefinition.customFieldId
|
|
?? normalizedDefinition.id
|
|
?? null,
|
|
customField: {
|
|
id: normalizedDefinition.id ?? normalizedDefinition.customFieldId ?? null,
|
|
name: normalizedDefinition.name,
|
|
type: normalizedDefinition.type,
|
|
required: normalizedDefinition.required,
|
|
options: normalizedDefinition.options,
|
|
defaultValue: normalizedDefinition.defaultValue ?? '',
|
|
},
|
|
value,
|
|
}
|
|
}
|
|
|
|
const mergeCustomFieldValuesWithDefinitions = (valueEntries = [], ...definitionSources) => {
|
|
const normalizedValues = (Array.isArray(valueEntries) ? valueEntries : [])
|
|
.map((entry) => {
|
|
if (!entry || typeof entry !== 'object') {
|
|
return null
|
|
}
|
|
|
|
const normalizedDefinition = normalizeCustomFieldDefinitionEntry(entry.customField || entry)
|
|
if (!normalizedDefinition) {
|
|
return null
|
|
}
|
|
|
|
const value = coerceValueForType(
|
|
normalizedDefinition.type,
|
|
entry?.value ?? entry?.defaultValue ?? normalizedDefinition.defaultValue ?? '',
|
|
)
|
|
|
|
return {
|
|
customFieldValueId: entry?.id ?? entry?.customFieldValueId ?? null,
|
|
id: normalizedDefinition.id,
|
|
customFieldId: normalizedDefinition.customFieldId ?? normalizedDefinition.id ?? null,
|
|
name: normalizedDefinition.name,
|
|
type: normalizedDefinition.type,
|
|
required: normalizedDefinition.required,
|
|
options: normalizedDefinition.options,
|
|
optionsText: normalizedDefinition.options?.length ? normalizedDefinition.options.join('\n') : '',
|
|
defaultValue: normalizedDefinition.defaultValue ?? '',
|
|
value,
|
|
readOnly: !!entry?.readOnly,
|
|
}
|
|
})
|
|
.filter((entry) => entry !== null)
|
|
|
|
const result = [...normalizedValues]
|
|
const keyFor = (item) => item?.id ?? `${item?.name ?? ''}::${item?.type ?? ''}`
|
|
const existingMap = new Map()
|
|
result.forEach((item) => {
|
|
const key = keyFor(item)
|
|
if (key) {
|
|
existingMap.set(key, item)
|
|
}
|
|
const fallbackKey = item?.name ? `${item.name}::${item.type ?? ''}` : null
|
|
if (fallbackKey) {
|
|
existingMap.set(fallbackKey, item)
|
|
}
|
|
})
|
|
|
|
const definitions = definitionSources
|
|
.flatMap((source) => (Array.isArray(source) ? source : []))
|
|
.map((definition) => normalizeCustomFieldDefinitionEntry(definition))
|
|
.filter((definition) => definition !== null)
|
|
|
|
const allowedKeys = new Set()
|
|
definitions.forEach((definition) => {
|
|
if (!definition) {
|
|
return
|
|
}
|
|
if (definition.id) {
|
|
allowedKeys.add(definition.id)
|
|
}
|
|
if (definition.customFieldId) {
|
|
allowedKeys.add(definition.customFieldId)
|
|
}
|
|
if (definition.name) {
|
|
allowedKeys.add(`${definition.name}::${definition.type}`)
|
|
}
|
|
})
|
|
|
|
definitions.forEach((normalizedDefinition) => {
|
|
const key = normalizedDefinition.id ?? `${normalizedDefinition.name}::${normalizedDefinition.type}`
|
|
if (!key) {
|
|
return
|
|
}
|
|
if (normalizedDefinition.id) {
|
|
const fallbackKey = `${normalizedDefinition.name}::${normalizedDefinition.type}`
|
|
if (existingMap.has(fallbackKey)) {
|
|
const existingFallback = existingMap.get(fallbackKey)
|
|
if (existingFallback) {
|
|
existingFallback.id = existingFallback.id || normalizedDefinition.id
|
|
existingFallback.customFieldId = normalizedDefinition.id
|
|
existingFallback.readOnly = existingFallback.readOnly && normalizedDefinition.readOnly
|
|
existingMap.delete(fallbackKey)
|
|
existingMap.set(normalizedDefinition.id, existingFallback)
|
|
existingMap.set(fallbackKey, existingFallback)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
const existing = existingMap.get(key) || (normalizedDefinition.name
|
|
? existingMap.get(`${normalizedDefinition.name}::${normalizedDefinition.type}`)
|
|
: null)
|
|
if (existing) {
|
|
existing.name = existing.name || normalizedDefinition.name
|
|
existing.type = existing.type || normalizedDefinition.type
|
|
existing.required = existing.required || normalizedDefinition.required
|
|
if (!existing.options?.length && normalizedDefinition.options?.length) {
|
|
existing.options = normalizedDefinition.options
|
|
}
|
|
if (!existing.defaultValue && normalizedDefinition.defaultValue) {
|
|
existing.defaultValue = String(normalizedDefinition.defaultValue)
|
|
if (!existing.value) {
|
|
existing.value = coerceValueForType(existing.type, normalizedDefinition.defaultValue)
|
|
}
|
|
}
|
|
existing.customFieldId = existing.customFieldId || normalizedDefinition.id
|
|
existing.readOnly = existing.readOnly && normalizedDefinition.readOnly
|
|
if (!existing.optionsText && normalizedDefinition.options?.length) {
|
|
existing.optionsText = normalizedDefinition.options.join('\n')
|
|
}
|
|
if (normalizedDefinition.id) {
|
|
existingMap.set(normalizedDefinition.id, existing)
|
|
}
|
|
if (normalizedDefinition.name) {
|
|
existingMap.set(`${normalizedDefinition.name}::${normalizedDefinition.type}`, existing)
|
|
}
|
|
return
|
|
}
|
|
|
|
const entry = {
|
|
customFieldValueId: null,
|
|
id: normalizedDefinition.id,
|
|
customFieldId: normalizedDefinition.id,
|
|
name: normalizedDefinition.name,
|
|
type: normalizedDefinition.type,
|
|
required: normalizedDefinition.required,
|
|
options: normalizedDefinition.options,
|
|
optionsText: normalizedDefinition.options?.length ? normalizedDefinition.options.join('\n') : '',
|
|
defaultValue: normalizedDefinition.defaultValue ?? '',
|
|
value: coerceValueForType(normalizedDefinition.type, normalizedDefinition.defaultValue ?? ''),
|
|
readOnly: false,
|
|
}
|
|
result.push(entry)
|
|
existingMap.set(key, entry)
|
|
const fallbackKey = entry.name ? `${entry.name}::${entry.type}` : null
|
|
if (fallbackKey) {
|
|
existingMap.set(fallbackKey, entry)
|
|
}
|
|
})
|
|
|
|
return result
|
|
}
|
|
|
|
const dedupeCustomFieldEntries = (fields) => {
|
|
if (!Array.isArray(fields) || fields.length <= 1) {
|
|
return Array.isArray(fields) ? fields : []
|
|
}
|
|
|
|
const seen = new Set()
|
|
const result = []
|
|
|
|
for (const field of fields) {
|
|
if (!field) {
|
|
continue
|
|
}
|
|
|
|
field.type = field.type || 'text'
|
|
|
|
let normalizedName =
|
|
typeof field.name === 'string' ? field.name.trim() : ''
|
|
|
|
if (!normalizedName && field.customField?.name) {
|
|
normalizedName = String(field.customField.name).trim()
|
|
field.name = normalizedName
|
|
} else if (typeof field.name === 'string') {
|
|
field.name = normalizedName
|
|
}
|
|
|
|
const key =
|
|
field.customFieldId ||
|
|
field.id ||
|
|
(normalizedName ? `${normalizedName}::${field.type || 'text'}` : null)
|
|
|
|
if (!key && !normalizedName) {
|
|
continue
|
|
}
|
|
|
|
if (key && seen.has(key)) {
|
|
continue
|
|
}
|
|
|
|
if (!normalizedName) {
|
|
continue
|
|
}
|
|
|
|
if (key) {
|
|
seen.add(key)
|
|
}
|
|
if (normalizedName) {
|
|
seen.add(`${normalizedName}::${field.type || 'text'}`)
|
|
}
|
|
result.push(field)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
const transformCustomFields = (pieces) => {
|
|
return (pieces || []).map((piece) => {
|
|
const requirement = piece.typeMachinePieceRequirement || {}
|
|
const typePiece = requirement.typePiece || piece.typePiece || {}
|
|
|
|
const normalizeStructureDefinitions = (structure) => (
|
|
structure ? normalizeStructureForEditor(structure) : null
|
|
)
|
|
|
|
const normalizedStructureDefinitions = [
|
|
normalizeStructureDefinitions(piece.definition?.structure),
|
|
normalizeStructureDefinitions(piece.typePiece?.structure),
|
|
normalizeStructureDefinitions(typePiece.structure),
|
|
normalizeStructureDefinitions(typePiece.pieceSkeleton),
|
|
normalizeStructureDefinitions(piece.typePiece?.pieceSkeleton),
|
|
normalizeStructureDefinitions(requirement.structure),
|
|
normalizeStructureDefinitions(requirement.pieceSkeleton),
|
|
]
|
|
|
|
const valueEntries = [
|
|
...(Array.isArray(piece.customFieldValues) ? piece.customFieldValues : []),
|
|
...(Array.isArray(piece.customFields)
|
|
? piece.customFields
|
|
.map(normalizeCustomFieldValueEntry)
|
|
.filter((entry) => entry !== null)
|
|
: []),
|
|
...(Array.isArray(typePiece.customFieldValues)
|
|
? typePiece.customFieldValues
|
|
.map(normalizeCustomFieldValueEntry)
|
|
.filter((entry) => entry !== null)
|
|
: []),
|
|
]
|
|
|
|
const customFields = dedupeCustomFieldEntries(
|
|
mergeCustomFieldValuesWithDefinitions(
|
|
valueEntries,
|
|
normalizeExistingCustomFieldDefinitions(piece.customFields),
|
|
normalizeExistingCustomFieldDefinitions(piece.definition?.customFields),
|
|
normalizeExistingCustomFieldDefinitions(piece.typePiece?.customFields),
|
|
normalizeExistingCustomFieldDefinitions(typePiece.customFields),
|
|
normalizeExistingCustomFieldDefinitions(requirement.typePiece?.customFields),
|
|
normalizeExistingCustomFieldDefinitions(requirement.customFields),
|
|
normalizeExistingCustomFieldDefinitions(requirement.definition?.customFields),
|
|
...normalizedStructureDefinitions.map((definition) =>
|
|
getStructureCustomFields(definition),
|
|
),
|
|
),
|
|
)
|
|
|
|
const constructeurIds = uniqueConstructeurIds(
|
|
piece.constructeurIds,
|
|
piece.constructeurId,
|
|
piece.constructeur,
|
|
piece.originalPiece?.constructeurIds,
|
|
piece.originalPiece?.constructeurId,
|
|
piece.originalPiece?.constructeur,
|
|
)
|
|
|
|
const { product: resolvedProduct, productId: resolvedProductId } = resolveProductReference(piece)
|
|
|
|
const constructeursList = resolveConstructeurs(
|
|
constructeurIds,
|
|
Array.isArray(piece.constructeurs) ? piece.constructeurs : [],
|
|
piece.constructeur ? [piece.constructeur] : [],
|
|
Array.isArray(piece.originalPiece?.constructeurs)
|
|
? piece.originalPiece?.constructeurs
|
|
: [],
|
|
piece.originalPiece?.constructeur ? [piece.originalPiece.constructeur] : [],
|
|
constructeurs.value,
|
|
)
|
|
const normalizedPiece = {
|
|
...piece,
|
|
product: resolvedProduct || piece.product || null,
|
|
productId:
|
|
resolvedProductId ||
|
|
piece.productId ||
|
|
piece.product?.id ||
|
|
null,
|
|
}
|
|
const productDisplay = getProductDisplay(normalizedPiece)
|
|
|
|
return {
|
|
...normalizedPiece,
|
|
customFields,
|
|
documents: piece.documents || [],
|
|
constructeurs: constructeursList,
|
|
constructeur: constructeursList[0] || piece.constructeur || null,
|
|
constructeurIds,
|
|
constructeurId: constructeurIds[0] || null,
|
|
typePieceId: piece.typePieceId
|
|
|| piece.typeMachinePieceRequirement?.typePieceId
|
|
|| piece.typePiece?.id
|
|
|| null,
|
|
__productDisplay: productDisplay,
|
|
}
|
|
})
|
|
}
|
|
|
|
// Transform custom fields for components (now handles nested structure)
|
|
const transformComponentCustomFields = (componentsData) => {
|
|
const normalizeStructureDefinitions = (structure) => (
|
|
structure ? normalizeStructureForEditor(structure) : null
|
|
)
|
|
|
|
return (componentsData || []).map((component) => {
|
|
const requirement = component.typeMachineComponentRequirement || {}
|
|
const type = requirement.typeComposant || component.typeComposant || {}
|
|
|
|
const normalizedStructureDefinitions = [
|
|
normalizeStructureDefinitions(component.definition?.structure),
|
|
normalizeStructureDefinitions(component.typeComposant?.structure),
|
|
normalizeStructureDefinitions(type.structure),
|
|
normalizeStructureDefinitions(type.componentSkeleton),
|
|
normalizeStructureDefinitions(requirement.structure),
|
|
normalizeStructureDefinitions(requirement.componentSkeleton),
|
|
]
|
|
|
|
const actualComponent = component.originalComposant || component
|
|
|
|
const valueEntries = [
|
|
...(Array.isArray(component.customFieldValues) ? component.customFieldValues : []),
|
|
...(Array.isArray(component.customFields)
|
|
? component.customFields
|
|
.map(normalizeCustomFieldValueEntry)
|
|
.filter((entry) => entry !== null)
|
|
: []),
|
|
...(Array.isArray(actualComponent?.customFields)
|
|
? actualComponent.customFields
|
|
.map(normalizeCustomFieldValueEntry)
|
|
.filter((entry) => entry !== null)
|
|
: []),
|
|
]
|
|
|
|
const customFields = dedupeCustomFieldEntries(
|
|
mergeCustomFieldValuesWithDefinitions(
|
|
valueEntries,
|
|
normalizeExistingCustomFieldDefinitions(component.customFields),
|
|
normalizeExistingCustomFieldDefinitions(component.definition?.customFields),
|
|
normalizeExistingCustomFieldDefinitions(component.typeComposant?.customFields),
|
|
normalizeExistingCustomFieldDefinitions(type.customFields),
|
|
normalizeExistingCustomFieldDefinitions(actualComponent?.customFields),
|
|
normalizeExistingCustomFieldDefinitions(requirement.typeComposant?.customFields),
|
|
normalizeExistingCustomFieldDefinitions(requirement.customFields),
|
|
normalizeExistingCustomFieldDefinitions(requirement.definition?.customFields),
|
|
...normalizedStructureDefinitions.map((definition) =>
|
|
getStructureCustomFields(definition),
|
|
),
|
|
),
|
|
)
|
|
|
|
const pieces = component.pieces
|
|
? transformCustomFields(component.pieces).map((piece) => ({
|
|
...piece,
|
|
parentComponentName: component.name,
|
|
}))
|
|
: []
|
|
|
|
const subComponents = component.sousComposants
|
|
? transformComponentCustomFields(component.sousComposants)
|
|
: []
|
|
|
|
const constructeurIds = uniqueConstructeurIds(
|
|
component.constructeurIds,
|
|
component.constructeurId,
|
|
component.constructeur,
|
|
actualComponent?.constructeurIds,
|
|
actualComponent?.constructeurId,
|
|
actualComponent?.constructeur,
|
|
)
|
|
|
|
const constructeursList = resolveConstructeurs(
|
|
constructeurIds,
|
|
Array.isArray(component.constructeurs) ? component.constructeurs : [],
|
|
component.constructeur ? [component.constructeur] : [],
|
|
Array.isArray(actualComponent?.constructeurs) ? actualComponent.constructeurs : [],
|
|
actualComponent?.constructeur ? [actualComponent.constructeur] : [],
|
|
constructeurs.value,
|
|
)
|
|
const { product: resolvedProduct, productId: resolvedProductId } = resolveProductReference(component)
|
|
const normalizedComponent = {
|
|
...component,
|
|
product: resolvedProduct || component.product || null,
|
|
productId:
|
|
resolvedProductId ||
|
|
component.productId ||
|
|
component.product?.id ||
|
|
null,
|
|
}
|
|
const productDisplay = getProductDisplay(normalizedComponent)
|
|
|
|
return {
|
|
...normalizedComponent,
|
|
customFields,
|
|
pieces,
|
|
subComponents,
|
|
documents: component.documents || [],
|
|
constructeurs: constructeursList,
|
|
constructeur: constructeursList[0] || component.constructeur || null,
|
|
constructeurIds,
|
|
constructeurId: constructeurIds[0] || null,
|
|
typeComposantId: component.typeComposantId
|
|
|| component.typeMachineComponentRequirement?.typeComposantId
|
|
|| component.typeComposant?.id
|
|
|| null,
|
|
__productDisplay: productDisplay,
|
|
}
|
|
})
|
|
}
|
|
|
|
const syncMachineCustomFields = () => {
|
|
if (!machine.value) {
|
|
machineCustomFields.value = []
|
|
return
|
|
}
|
|
|
|
const valueEntries = [
|
|
...(Array.isArray(machine.value.customFieldValues) ? machine.value.customFieldValues : []),
|
|
...(Array.isArray(machine.value.customFields)
|
|
? machine.value.customFields
|
|
.map(normalizeCustomFieldValueEntry)
|
|
.filter((entry) => entry !== null)
|
|
: []),
|
|
]
|
|
|
|
const merged = dedupeCustomFieldEntries(
|
|
mergeCustomFieldValuesWithDefinitions(
|
|
valueEntries,
|
|
normalizeExistingCustomFieldDefinitions(machine.value.customFields),
|
|
normalizeExistingCustomFieldDefinitions(machine.value.typeMachine?.customFields),
|
|
),
|
|
).map((field) => ({
|
|
...field,
|
|
readOnly: false,
|
|
}))
|
|
|
|
machineCustomFields.value = merged
|
|
}
|
|
|
|
function mergePieceLists(existing = [], updates = []) {
|
|
if (!existing.length) {
|
|
return updates.map(piece => ({
|
|
...piece,
|
|
constructeurs: piece.constructeurs || [],
|
|
}))
|
|
}
|
|
if (!updates.length) {
|
|
return existing.map(piece => ({
|
|
...piece,
|
|
constructeurs: piece.constructeurs || [],
|
|
}))
|
|
}
|
|
|
|
const updateMap = new Map(
|
|
updates.map(piece => [
|
|
piece.id,
|
|
{
|
|
...piece,
|
|
constructeurs: piece.constructeurs || [],
|
|
},
|
|
]),
|
|
)
|
|
const merged = existing.map(piece => {
|
|
const update = updateMap.get(piece.id)
|
|
if (!update) {
|
|
return piece
|
|
}
|
|
return {
|
|
...piece,
|
|
...update,
|
|
customFields: update.customFields ?? piece.customFields,
|
|
}
|
|
})
|
|
|
|
updates.forEach(update => {
|
|
if (!existing.some(piece => piece.id === update.id)) {
|
|
merged.push(update)
|
|
}
|
|
})
|
|
|
|
return merged
|
|
}
|
|
|
|
function mergeComponentTrees(existing = [], updates = []) {
|
|
if (!existing.length) {
|
|
return updates.map(component => ({
|
|
...component,
|
|
constructeurs: component.constructeurs || [],
|
|
pieces: (component.pieces || []).map(piece => ({
|
|
...piece,
|
|
constructeurs: piece.constructeurs || [],
|
|
})),
|
|
subComponents: mergeComponentTrees([], component.subComponents || []),
|
|
}))
|
|
}
|
|
if (!updates.length) {
|
|
return existing
|
|
}
|
|
|
|
const updateMap = new Map(
|
|
updates.map(component => [
|
|
component.id,
|
|
{
|
|
...component,
|
|
constructeurs: component.constructeurs || [],
|
|
pieces: (component.pieces || []).map(piece => ({
|
|
...piece,
|
|
constructeurs: piece.constructeurs || [],
|
|
})),
|
|
subComponents: mergeComponentTrees([], component.subComponents || []),
|
|
},
|
|
]),
|
|
)
|
|
const merged = existing.map(component => {
|
|
const update = updateMap.get(component.id)
|
|
if (!update) {
|
|
return {
|
|
...component,
|
|
constructeurs: component.constructeurs || [],
|
|
pieces: mergePieceLists(component.pieces || [], []),
|
|
subComponents: mergeComponentTrees(component.subComponents || [], []),
|
|
}
|
|
}
|
|
return {
|
|
...component,
|
|
...update,
|
|
customFields: update.customFields ?? component.customFields,
|
|
pieces: mergePieceLists(component.pieces || [], update.pieces || []),
|
|
subComponents: mergeComponentTrees(component.subComponents || [], update.subComponents || []),
|
|
}
|
|
})
|
|
|
|
updates.forEach(update => {
|
|
if (!existing.some(component => component.id === update.id)) {
|
|
merged.push(update)
|
|
}
|
|
})
|
|
|
|
return merged
|
|
}
|
|
|
|
const buildMachineHierarchyFromLinks = (componentLinks = [], pieceLinks = []) => {
|
|
const normalizeComponentLinkId = (link) =>
|
|
resolveIdentifier(link?.id, link?.linkId, link?.machineComponentLinkId)
|
|
|
|
const normalizePieceLinkId = (link) =>
|
|
resolveIdentifier(link?.id, link?.linkId, link?.machinePieceLinkId)
|
|
|
|
const createPieceNode = (link, parentComponentName = null) => {
|
|
if (!link || typeof link !== 'object') {
|
|
return null
|
|
}
|
|
|
|
const appliedPiece =
|
|
(link.piece && typeof link.piece === 'object' && link.piece) || {}
|
|
const originalPiece =
|
|
(link.originalPiece && typeof link.originalPiece === 'object' && link.originalPiece) || null
|
|
|
|
const requirement =
|
|
link.typeMachinePieceRequirement ||
|
|
appliedPiece.typeMachinePieceRequirement ||
|
|
originalPiece?.typeMachinePieceRequirement ||
|
|
null
|
|
|
|
const machinePieceLinkId = normalizePieceLinkId(link)
|
|
const pieceId = resolveIdentifier(appliedPiece.id, appliedPiece.pieceId, link.pieceId)
|
|
|
|
const basePiece = {
|
|
...appliedPiece,
|
|
id: appliedPiece.id || pieceId || machinePieceLinkId || `piece-${machinePieceLinkId}`,
|
|
pieceId,
|
|
name:
|
|
link.overrides?.name ||
|
|
appliedPiece.name ||
|
|
appliedPiece.definition?.name ||
|
|
appliedPiece.definition?.role ||
|
|
originalPiece?.name ||
|
|
'Pièce',
|
|
reference:
|
|
link.overrides?.reference ||
|
|
appliedPiece.reference ||
|
|
appliedPiece.definition?.reference ||
|
|
originalPiece?.reference ||
|
|
null,
|
|
prix:
|
|
link.overrides?.prix ??
|
|
appliedPiece.prix ??
|
|
originalPiece?.prix ??
|
|
null,
|
|
constructeur:
|
|
appliedPiece.constructeur ||
|
|
originalPiece?.constructeur ||
|
|
null,
|
|
constructeurId:
|
|
appliedPiece.constructeurId ||
|
|
appliedPiece.constructeur?.id ||
|
|
originalPiece?.constructeurId ||
|
|
null,
|
|
documents:
|
|
Array.isArray(appliedPiece.documents)
|
|
? appliedPiece.documents
|
|
: Array.isArray(originalPiece?.documents)
|
|
? originalPiece.documents
|
|
: [],
|
|
typePiece: appliedPiece.typePiece || requirement?.typePiece || null,
|
|
typePieceId:
|
|
appliedPiece.typePieceId ||
|
|
appliedPiece.typePiece?.id ||
|
|
requirement?.typePieceId ||
|
|
requirement?.typePiece?.id ||
|
|
null,
|
|
typeMachinePieceRequirement: requirement,
|
|
typeMachinePieceRequirementId: requirement?.id || null,
|
|
requirementId: requirement?.id || null,
|
|
overrides: link.overrides || null,
|
|
originalPiece,
|
|
machinePieceLink: link,
|
|
machinePieceLinkId,
|
|
linkId: machinePieceLinkId,
|
|
parentComponentLinkId: resolveIdentifier(
|
|
link.parentComponentLinkId,
|
|
link.parentLinkId,
|
|
link.parentMachineComponentLinkId,
|
|
appliedPiece.parentComponentLinkId,
|
|
),
|
|
parentComponentId: resolveIdentifier(
|
|
appliedPiece.parentComponentId,
|
|
link.parentComponentId,
|
|
),
|
|
parentComponentName,
|
|
parentLinkId: resolveIdentifier(
|
|
link.parentLinkId,
|
|
link.parentMachinePieceLinkId,
|
|
appliedPiece.parentLinkId,
|
|
),
|
|
parentPieceLinkId: resolveIdentifier(
|
|
link.parentPieceLinkId,
|
|
appliedPiece.parentPieceLinkId,
|
|
),
|
|
parentPieceId: resolveIdentifier(
|
|
appliedPiece.parentPieceId,
|
|
link.parentPieceId,
|
|
),
|
|
parentMachineComponentRequirementId: resolveIdentifier(
|
|
appliedPiece.parentMachineComponentRequirementId,
|
|
link.parentMachineComponentRequirementId,
|
|
),
|
|
parentMachinePieceRequirementId: resolveIdentifier(
|
|
appliedPiece.parentMachinePieceRequirementId,
|
|
link.parentMachinePieceRequirementId,
|
|
),
|
|
definition: appliedPiece.definition || originalPiece?.definition || {},
|
|
customFields: appliedPiece.customFields || [],
|
|
skeletonOnly: !pieceId,
|
|
}
|
|
|
|
const resolvedProductId = resolveIdentifier(
|
|
appliedPiece.productId,
|
|
appliedPiece.product?.id,
|
|
link.productId,
|
|
link.product?.id,
|
|
originalPiece?.productId,
|
|
originalPiece?.product?.id,
|
|
)
|
|
|
|
const resolvedProduct =
|
|
appliedPiece.product ||
|
|
link.product ||
|
|
originalPiece?.product ||
|
|
(resolvedProductId ? findProductById(resolvedProductId) : null) ||
|
|
null
|
|
|
|
const constructeurs = collectConstructeurs(
|
|
appliedPiece.constructeurs,
|
|
appliedPiece.constructeur,
|
|
appliedPiece.constructeurIds,
|
|
appliedPiece.constructeurId,
|
|
originalPiece?.constructeurs,
|
|
originalPiece?.constructeur,
|
|
originalPiece?.constructeurIds,
|
|
originalPiece?.constructeurId,
|
|
)
|
|
|
|
return {
|
|
...basePiece,
|
|
constructeurs,
|
|
constructeur: constructeurs[0] || basePiece.constructeur || null,
|
|
constructeurId: constructeurs[0]?.id || basePiece.constructeurId || null,
|
|
productId: resolvedProductId || appliedPiece.productId || null,
|
|
product: resolvedProduct || appliedPiece.product || null,
|
|
__productDisplay: getProductDisplay({
|
|
product: resolvedProduct || appliedPiece.product || null,
|
|
productId: resolvedProductId || appliedPiece.productId || null,
|
|
}),
|
|
}
|
|
}
|
|
|
|
const createComponentNode = (link) => {
|
|
if (!link || typeof link !== 'object') {
|
|
return null
|
|
}
|
|
|
|
const appliedComponent =
|
|
(link.composant && typeof link.composant === 'object' && link.composant) || {}
|
|
const originalComponent =
|
|
(link.originalComposant && typeof link.originalComposant === 'object' && link.originalComposant) || null
|
|
|
|
const requirement =
|
|
link.typeMachineComponentRequirement ||
|
|
appliedComponent.typeMachineComponentRequirement ||
|
|
originalComponent?.typeMachineComponentRequirement ||
|
|
null
|
|
|
|
const machineComponentLinkId = normalizeComponentLinkId(link)
|
|
const composantId = resolveIdentifier(
|
|
appliedComponent.id,
|
|
appliedComponent.composantId,
|
|
link.composantId,
|
|
)
|
|
|
|
const componentName =
|
|
link.overrides?.name ||
|
|
appliedComponent.name ||
|
|
appliedComponent.definition?.alias ||
|
|
appliedComponent.definition?.name ||
|
|
originalComponent?.name ||
|
|
'Composant'
|
|
|
|
const pieces = Array.isArray(link.pieceLinks)
|
|
? link.pieceLinks.map((pieceLink) => createPieceNode(pieceLink, componentName)).filter(Boolean)
|
|
: []
|
|
|
|
const subComponents = Array.isArray(link.childLinks)
|
|
? link.childLinks.map(createComponentNode).filter(Boolean)
|
|
: []
|
|
|
|
const resolvedProductId = resolveIdentifier(
|
|
appliedComponent.productId,
|
|
appliedComponent.product?.id,
|
|
link.productId,
|
|
link.product?.id,
|
|
originalComponent?.productId,
|
|
originalComponent?.product?.id,
|
|
)
|
|
|
|
const resolvedProduct =
|
|
appliedComponent.product ||
|
|
link.product ||
|
|
originalComponent?.product ||
|
|
(resolvedProductId ? findProductById(resolvedProductId) : null) ||
|
|
null
|
|
|
|
const baseComponent = {
|
|
...appliedComponent,
|
|
id: appliedComponent.id || composantId || machineComponentLinkId || `component-${machineComponentLinkId}`,
|
|
composantId,
|
|
name: componentName,
|
|
reference:
|
|
link.overrides?.reference ||
|
|
appliedComponent.reference ||
|
|
appliedComponent.definition?.reference ||
|
|
originalComponent?.reference ||
|
|
null,
|
|
prix:
|
|
link.overrides?.prix ??
|
|
appliedComponent.prix ??
|
|
originalComponent?.prix ??
|
|
null,
|
|
constructeur:
|
|
appliedComponent.constructeur ||
|
|
originalComponent?.constructeur ||
|
|
null,
|
|
constructeurId:
|
|
appliedComponent.constructeurId ||
|
|
appliedComponent.constructeur?.id ||
|
|
originalComponent?.constructeurId ||
|
|
null,
|
|
documents:
|
|
Array.isArray(appliedComponent.documents)
|
|
? appliedComponent.documents
|
|
: Array.isArray(originalComponent?.documents)
|
|
? originalComponent.documents
|
|
: [],
|
|
typeComposant:
|
|
appliedComponent.typeComposant ||
|
|
requirement?.typeComposant ||
|
|
null,
|
|
typeComposantId:
|
|
appliedComponent.typeComposantId ||
|
|
appliedComponent.typeComposant?.id ||
|
|
requirement?.typeComposantId ||
|
|
requirement?.typeComposant?.id ||
|
|
null,
|
|
typeMachineComponentRequirement: requirement,
|
|
typeMachineComponentRequirementId: requirement?.id || null,
|
|
requirementId: requirement?.id || null,
|
|
overrides: link.overrides || null,
|
|
machineComponentLinkOverrides: link.overrides || null,
|
|
definitionOverrides: link.overrides || null,
|
|
originalComposant: originalComponent,
|
|
machineComponentLink: link,
|
|
machineComponentLinkId,
|
|
componentLinkId: machineComponentLinkId,
|
|
parentComponentLinkId: resolveIdentifier(
|
|
link.parentComponentLinkId,
|
|
link.parentLinkId,
|
|
link.parentMachineComponentLinkId,
|
|
appliedComponent.parentComponentLinkId,
|
|
),
|
|
parentComposantId: resolveIdentifier(
|
|
appliedComponent.parentComposantId,
|
|
link.parentComponentId,
|
|
),
|
|
parentRequirementId: resolveIdentifier(
|
|
appliedComponent.parentRequirementId,
|
|
link.parentRequirementId,
|
|
),
|
|
parentMachineComponentRequirementId: resolveIdentifier(
|
|
appliedComponent.parentMachineComponentRequirementId,
|
|
link.parentMachineComponentRequirementId,
|
|
),
|
|
parentMachinePieceRequirementId: resolveIdentifier(
|
|
appliedComponent.parentMachinePieceRequirementId,
|
|
link.parentMachinePieceRequirementId,
|
|
),
|
|
definition: appliedComponent.definition || originalComponent?.definition || {},
|
|
customFields: appliedComponent.customFields || [],
|
|
pieces,
|
|
subComponents,
|
|
subcomponents: subComponents,
|
|
sousComposants: subComponents,
|
|
skeletonOnly: !composantId,
|
|
}
|
|
|
|
const constructeurs = collectConstructeurs(
|
|
appliedComponent.constructeurs,
|
|
appliedComponent.constructeur,
|
|
appliedComponent.constructeurIds,
|
|
appliedComponent.constructeurId,
|
|
originalComponent?.constructeurs,
|
|
originalComponent?.constructeur,
|
|
originalComponent?.constructeurIds,
|
|
originalComponent?.constructeurId,
|
|
)
|
|
|
|
return {
|
|
...baseComponent,
|
|
constructeurs,
|
|
constructeur: constructeurs[0] || baseComponent.constructeur || null,
|
|
constructeurId: constructeurs[0]?.id || baseComponent.constructeurId || null,
|
|
productId: resolvedProductId || appliedComponent.productId || null,
|
|
product: resolvedProduct || appliedComponent.product || null,
|
|
__productDisplay: getProductDisplay({
|
|
product: resolvedProduct || appliedComponent.product || null,
|
|
productId: resolvedProductId || appliedComponent.productId || null,
|
|
}),
|
|
}
|
|
}
|
|
|
|
const rootComponents = (Array.isArray(componentLinks) ? componentLinks : [])
|
|
.filter((link) =>
|
|
!resolveIdentifier(
|
|
link?.parentComponentLinkId,
|
|
link?.parentLinkId,
|
|
link?.parentMachineComponentLinkId,
|
|
),
|
|
)
|
|
.map(createComponentNode)
|
|
.filter(Boolean)
|
|
|
|
const machinePieces = (Array.isArray(pieceLinks) ? pieceLinks : [])
|
|
.filter((link) =>
|
|
!resolveIdentifier(
|
|
link?.parentComponentLinkId,
|
|
link?.parentLinkId,
|
|
link?.parentMachineComponentLinkId,
|
|
),
|
|
)
|
|
.map((link) => createPieceNode(link, null))
|
|
.filter(Boolean)
|
|
|
|
return {
|
|
components: rootComponents,
|
|
machinePieces,
|
|
}
|
|
}
|
|
|
|
const resolveLinkArray = (source, keys) => {
|
|
if (!source || typeof source !== 'object') {
|
|
return null
|
|
}
|
|
for (const key of keys) {
|
|
const value = source[key]
|
|
if (Array.isArray(value)) {
|
|
return value
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
const applyMachineLinks = (source) => {
|
|
const container = source?.machine ?? null
|
|
const componentLinks =
|
|
resolveLinkArray(source, ['componentLinks', 'machineComponentLinks']) ??
|
|
resolveLinkArray(container, ['componentLinks', 'machineComponentLinks'])
|
|
const pieceLinks =
|
|
resolveLinkArray(source, ['pieceLinks', 'machinePieceLinks']) ??
|
|
resolveLinkArray(container, ['pieceLinks', 'machinePieceLinks'])
|
|
const productLinks =
|
|
resolveLinkArray(source, ['productLinks', 'machineProductLinks']) ??
|
|
resolveLinkArray(container, ['productLinks', 'machineProductLinks'])
|
|
|
|
if (componentLinks === null && pieceLinks === null && productLinks === null) {
|
|
return false
|
|
}
|
|
|
|
const normalizedComponentLinks = componentLinks ?? []
|
|
const normalizedPieceLinks = pieceLinks ?? []
|
|
const normalizedProductLinks = productLinks ?? []
|
|
|
|
machineComponentLinks.value = normalizedComponentLinks
|
|
machinePieceLinks.value = normalizedPieceLinks
|
|
machineProductLinks.value = normalizedProductLinks
|
|
|
|
const { components: hierarchy, machinePieces: machineLevelPieces } =
|
|
buildMachineHierarchyFromLinks(normalizedComponentLinks, normalizedPieceLinks)
|
|
|
|
components.value = transformComponentCustomFields(hierarchy)
|
|
pieces.value = transformCustomFields(machineLevelPieces)
|
|
|
|
return true
|
|
}
|
|
|
|
// Methods
|
|
const loadMachineData = async () => {
|
|
loading.value = true
|
|
try {
|
|
const machineResult = await get(`/machines/${machineId}`)
|
|
|
|
if (!machineResult.success) {
|
|
console.error('Machine non trouvée:', machineId, machineResult.error)
|
|
machine.value = null
|
|
components.value = []
|
|
pieces.value = []
|
|
return
|
|
}
|
|
|
|
const machinePayload = machineResult.data?.machine && typeof machineResult.data.machine === 'object'
|
|
? machineResult.data.machine
|
|
: machineResult.data
|
|
|
|
if (!machinePayload || typeof machinePayload !== 'object') {
|
|
console.error('Réponse machine invalide pour', machineId)
|
|
machine.value = null
|
|
components.value = []
|
|
pieces.value = []
|
|
return
|
|
}
|
|
|
|
machine.value = {
|
|
...machinePayload,
|
|
documents: machinePayload.documents || [],
|
|
customFieldValues: machinePayload.customFieldValues || [],
|
|
}
|
|
|
|
machineDocumentsLoaded.value = machine.value.documents.length > 0
|
|
syncMachineCustomFields()
|
|
initMachineFields()
|
|
|
|
if (!productInventory.value.length) {
|
|
try {
|
|
await loadProducts()
|
|
} catch (error) {
|
|
console.error('Erreur lors du chargement des produits:', error)
|
|
}
|
|
}
|
|
|
|
const linksApplied = applyMachineLinks(machineResult.data)
|
|
|
|
if (machine.value) {
|
|
machine.value.componentLinks = machineComponentLinks.value
|
|
machine.value.pieceLinks = machinePieceLinks.value
|
|
machine.value.productLinks = machineProductLinks.value
|
|
}
|
|
|
|
if (!linksApplied) {
|
|
components.value = transformComponentCustomFields(machinePayload.components || [])
|
|
pieces.value = transformCustomFields(machinePayload.pieces || [])
|
|
machineProductLinks.value = Array.isArray(machinePayload.productLinks)
|
|
? machinePayload.productLinks
|
|
: []
|
|
}
|
|
|
|
if (machine.value) {
|
|
machine.value.productLinks = machineProductLinks.value
|
|
}
|
|
|
|
collapseAllComponents()
|
|
|
|
if (!machineDocumentsLoaded.value) {
|
|
await refreshMachineDocuments()
|
|
}
|
|
} catch (error) {
|
|
console.error('Erreur lors du chargement des données:', error)
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
const updateMachineInfo = async () => {
|
|
if (!machine.value) return
|
|
|
|
try {
|
|
const constructeurIds = uniqueConstructeurIds(machineConstructeurIds.value)
|
|
machineConstructeurIds.value = constructeurIds
|
|
|
|
const result = await updateMachineApi(machine.value.id, {
|
|
name: machineName.value,
|
|
reference: machineReference.value,
|
|
constructeurIds,
|
|
})
|
|
if (result.success) {
|
|
const machinePayload = result.data?.machine && typeof result.data.machine === 'object'
|
|
? result.data.machine
|
|
: result.data
|
|
|
|
if (machinePayload && typeof machinePayload === 'object') {
|
|
machine.value = {
|
|
...machine.value,
|
|
...machinePayload,
|
|
documents: machinePayload.documents || machine.value.documents || [],
|
|
customFieldValues: machinePayload.customFieldValues || machine.value.customFieldValues || [],
|
|
}
|
|
machineConstructeurIds.value = uniqueConstructeurIds(
|
|
machine.value.constructeurIds,
|
|
machine.value.constructeurs,
|
|
machine.value.constructeur,
|
|
)
|
|
|
|
const linksApplied = applyMachineLinks(result.data)
|
|
if (linksApplied && machine.value) {
|
|
machine.value.componentLinks = machineComponentLinks.value
|
|
machine.value.pieceLinks = machinePieceLinks.value
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Erreur lors de la mise à jour de la machine:', error)
|
|
}
|
|
}
|
|
|
|
const updateComponent = async (updatedComponent) => {
|
|
try {
|
|
const constructeurIds = uniqueConstructeurIds(
|
|
updatedComponent.constructeurIds,
|
|
updatedComponent.constructeurId,
|
|
updatedComponent.constructeur,
|
|
)
|
|
const productId = updatedComponent.productId ? String(updatedComponent.productId) : null
|
|
const prix =
|
|
updatedComponent.prix !== null &&
|
|
updatedComponent.prix !== undefined &&
|
|
String(updatedComponent.prix).trim() !== ''
|
|
? Number(updatedComponent.prix)
|
|
: null
|
|
|
|
const result = await updateComposantApi(updatedComponent.id, {
|
|
name: updatedComponent.name,
|
|
reference: updatedComponent.reference,
|
|
constructeurIds,
|
|
prix: Number.isNaN(prix) ? null : prix,
|
|
productId,
|
|
})
|
|
if (result.success) {
|
|
const transformed = transformComponentCustomFields([result.data])[0]
|
|
Object.assign(updatedComponent, transformed)
|
|
}
|
|
} catch (error) {
|
|
console.error('Erreur lors de la mise à jour du composant:', error)
|
|
}
|
|
}
|
|
|
|
const updatePieceFromComponent = async (updatedPiece) => {
|
|
try {
|
|
const constructeurIds = uniqueConstructeurIds(
|
|
updatedPiece.constructeurIds,
|
|
updatedPiece.constructeurId,
|
|
updatedPiece.constructeur,
|
|
)
|
|
const productId = updatedPiece.productId ? String(updatedPiece.productId) : null
|
|
const prix =
|
|
updatedPiece.prix !== null &&
|
|
updatedPiece.prix !== undefined &&
|
|
String(updatedPiece.prix).trim() !== ''
|
|
? Number(updatedPiece.prix)
|
|
: null
|
|
|
|
const result = await updatePieceApi(updatedPiece.id, {
|
|
name: updatedPiece.name,
|
|
reference: updatedPiece.reference,
|
|
constructeurIds,
|
|
prix: Number.isNaN(prix) ? null : prix,
|
|
productId,
|
|
})
|
|
if (result.success) {
|
|
const transformed = transformCustomFields([result.data])[0]
|
|
Object.assign(updatedPiece, transformed)
|
|
// Si la pièce a des champs personnalisés mis à jour, les traiter
|
|
if (updatedPiece.customFields) {
|
|
for (const field of updatedPiece.customFields) {
|
|
if (field.value !== undefined) {
|
|
await upsertCustomFieldValue(
|
|
field.id,
|
|
'piece',
|
|
updatedPiece.id,
|
|
field.value
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Erreur lors de la mise à jour de la pièce:', error)
|
|
}
|
|
}
|
|
|
|
const updatePieceInfo = async (updatedPiece) => {
|
|
try {
|
|
const constructeurIds = uniqueConstructeurIds(
|
|
updatedPiece.constructeurIds,
|
|
updatedPiece.constructeurId,
|
|
updatedPiece.constructeur,
|
|
)
|
|
const productId = updatedPiece.productId ? String(updatedPiece.productId) : null
|
|
const prix =
|
|
updatedPiece.prix !== null &&
|
|
updatedPiece.prix !== undefined &&
|
|
String(updatedPiece.prix).trim() !== ''
|
|
? Number(updatedPiece.prix)
|
|
: null
|
|
|
|
const result = await updatePieceApi(updatedPiece.id, {
|
|
name: updatedPiece.name,
|
|
reference: updatedPiece.reference,
|
|
constructeurIds,
|
|
prix: Number.isNaN(prix) ? null : prix,
|
|
productId,
|
|
})
|
|
if (result.success) {
|
|
const transformed = transformCustomFields([result.data])[0]
|
|
Object.assign(updatedPiece, transformed)
|
|
}
|
|
} catch (error) {
|
|
console.error('Erreur lors de la mise à jour de la pièce:', error)
|
|
}
|
|
}
|
|
|
|
// Méthodes pour les champs personnalisés de la machine
|
|
const setMachineCustomFieldValue = (field, value) => {
|
|
if (!field) {
|
|
return
|
|
}
|
|
|
|
field.value = value
|
|
|
|
if (field.customFieldValueId && machine.value?.customFieldValues) {
|
|
const stored = machine.value.customFieldValues.find((fv) => fv.id === field.customFieldValueId)
|
|
if (stored) {
|
|
stored.value = value
|
|
}
|
|
}
|
|
}
|
|
|
|
const updateMachineCustomField = async (field) => {
|
|
if (!machine.value || !field) {
|
|
return
|
|
}
|
|
|
|
const { id: customFieldId, customFieldValueId } = field
|
|
const fieldLabel = field.name || 'Champ personnalisé'
|
|
|
|
try {
|
|
if (customFieldValueId) {
|
|
const result = await updateCustomFieldValueApi(customFieldValueId, { value: field.value ?? '' })
|
|
if (result.success) {
|
|
toast.showSuccess(`Champ "${fieldLabel}" de la machine mis à jour avec succès`)
|
|
syncMachineCustomFields()
|
|
} else {
|
|
toast.showError(`Erreur lors de la mise à jour du champ "${fieldLabel}"`)
|
|
}
|
|
return
|
|
}
|
|
|
|
if (!customFieldId) {
|
|
toast.showError('Impossible de mettre à jour ce champ personnalisé (identifiant manquant).')
|
|
return
|
|
}
|
|
|
|
const result = await upsertCustomFieldValue(customFieldId, 'machine', machine.value.id, field.value ?? '')
|
|
if (result.success) {
|
|
const createdValue = result.data
|
|
toast.showSuccess(`Champ "${fieldLabel}" de la machine mis à jour avec succès`)
|
|
|
|
if (createdValue?.id) {
|
|
if (!createdValue.customField) {
|
|
createdValue.customField = {
|
|
id: customFieldId,
|
|
name: field.name,
|
|
type: field.type,
|
|
required: field.required,
|
|
options: field.options,
|
|
}
|
|
}
|
|
field.customFieldValueId = createdValue.id
|
|
field.readOnly = false
|
|
|
|
const existingValues = Array.isArray(machine.value.customFieldValues)
|
|
? machine.value.customFieldValues.filter((item) => item.id !== createdValue.id)
|
|
: []
|
|
|
|
machine.value.customFieldValues = [...existingValues, createdValue]
|
|
}
|
|
|
|
syncMachineCustomFields()
|
|
} else {
|
|
toast.showError(`Erreur lors de la mise à jour du champ "${fieldLabel}"`)
|
|
}
|
|
} catch (error) {
|
|
console.error('Erreur lors de la mise à jour du champ personnalisé de la machine:', error)
|
|
toast.showError(`Erreur lors de la mise à jour du champ "${fieldLabel}"`)
|
|
}
|
|
}
|
|
|
|
const updatePieceCustomField = async (fieldUpdate) => {
|
|
const { showSuccess, showError } = useToast()
|
|
|
|
try {
|
|
const result = await upsertCustomFieldValue(
|
|
fieldUpdate.fieldId,
|
|
'piece',
|
|
fieldUpdate.pieceId,
|
|
fieldUpdate.value
|
|
)
|
|
if (result.success) {
|
|
showSuccess(`Champ personnalisé mis à jour avec succès`)
|
|
} else {
|
|
showError(`Erreur lors de la mise à jour du champ personnalisé`)
|
|
}
|
|
} catch (error) {
|
|
showError(`Erreur lors de la mise à jour du champ personnalisé`)
|
|
console.error('Erreur lors de la mise à jour du champ personnalisé:', error)
|
|
}
|
|
}
|
|
|
|
const editComponent = () => {
|
|
toast.showInfo('La modification des composants sera bientôt disponible')
|
|
}
|
|
|
|
const editPiece = () => {
|
|
toast.showInfo('La modification des pièces sera bientôt disponible')
|
|
}
|
|
|
|
const toggleEditMode = () => {
|
|
isEditMode.value = !isEditMode.value
|
|
debug.value = !debug.value // Inversez la valeur de debug
|
|
if (isEditMode.value && !machineDocumentsLoaded.value) {
|
|
refreshMachineDocuments()
|
|
}
|
|
}
|
|
|
|
watch(
|
|
() => machine.value?.customFieldValues,
|
|
() => {
|
|
syncMachineCustomFields()
|
|
},
|
|
{ deep: true }
|
|
)
|
|
|
|
watch(
|
|
() => machine.value?.customFields,
|
|
() => {
|
|
syncMachineCustomFields()
|
|
},
|
|
{ deep: true }
|
|
)
|
|
|
|
watch(
|
|
() => machine.value?.typeMachine?.customFields,
|
|
() => {
|
|
syncMachineCustomFields()
|
|
},
|
|
{ deep: true }
|
|
)
|
|
|
|
watch(
|
|
() => [components.value.length, machinePieces.value.length],
|
|
() => {
|
|
ensurePrintSelectionEntries()
|
|
},
|
|
{ immediate: true }
|
|
)
|
|
|
|
// Lifecycle
|
|
onMounted(() => {
|
|
loadMachineData()
|
|
if (!constructeurs.value.length) {
|
|
loadConstructeurs()
|
|
}
|
|
if (!componentTypes.value.length) {
|
|
loadComponentTypes()
|
|
}
|
|
if (!pieceTypes.value.length) {
|
|
loadPieceTypes()
|
|
}
|
|
|
|
// Vérifier si on doit activer le mode édition depuis l'URL
|
|
const route = useRoute()
|
|
if (route.query.edit === 'true') {
|
|
isEditMode.value = true
|
|
}
|
|
})
|
|
</script>
|