Files
Inventory_frontend/app/pages/machine/[id].vue
Matthieu 1f2d6c78e8 refacto(F1.1) : wire [id].vue to use extracted modules and fix TS errors
Wire machine/[id].vue to import from extracted utility modules
(customFieldUtils, productDisplayUtils, useMachineHierarchy, useMachinePrint).
Remove ~1400 LOC of inline functions replaced by imports.
Fix TypeScript errors in extracted composables (AnyRecord/ConstructeurSummary
boundary casts, Map generics, optional chaining on unknown).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 08:58:42 +01:00

2990 lines
99 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 } 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 {
coerceValueForType,
formatCustomFieldValue,
shouldDisplayCustomField,
normalizeCustomFieldDefinitionEntry,
normalizeExistingCustomFieldDefinitions,
normalizeCustomFieldValueEntry,
mergeCustomFieldValuesWithDefinitions,
dedupeCustomFieldEntries,
summarizeCustomFields,
} from '~/shared/utils/customFieldUtils'
import {
resolveIdentifier,
resolveProductReference as _resolveProductReference,
getProductDisplay as _getProductDisplay,
getProductSuppliersLabel,
getProductPriceLabel,
extractParentLinkIdentifiers,
} from '~/shared/utils/productDisplayUtils'
import {
buildMachineHierarchyFromLinks,
resolveLinkArray,
mergePieceLists,
mergeComponentTrees,
} from '~/composables/useMachineHierarchy'
import { useMachinePrint } from '~/composables/useMachinePrint'
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 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,
printSelection,
ensurePrintSelectionEntries: _ensurePrintEntries,
setAllPrintSelection: _setAllPrint,
openPrintModal: _openPrintModal,
closePrintModal,
handlePrintConfirm: _handlePrintConfirm,
} = useMachinePrint()
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
}
// Local wrappers that bind closures for imported functions
const resolveProductReference = (source) => _resolveProductReference(source, findProductById)
const getProductDisplay = (source) => _getProductDisplay(source, findProductById)
// getProductSuppliersLabel → imported from productDisplayUtils
// getProductPriceLabel → imported from productDisplayUtils
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]'
// resolveIdentifier → imported from productDisplayUtils
// extractParentLinkIdentifiers → imported from productDisplayUtils
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
}
// Print functions → imported from useMachinePrint composable
const ensurePrintSelectionEntries = () => _ensurePrintEntries(components.value, machinePieces.value)
const setAllPrintSelection = (value) => _setAllPrint(value, components.value, machinePieces.value)
const openPrintModal = () => _openPrintModal(components.value, machinePieces.value)
const handlePrintConfirm = () => _handlePrintConfirm(machine.value, machineName.value, machineReference.value, machinePieces.value, components.value)
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'
}
// formatCustomFieldValue, shouldDisplayCustomField → imported from customFieldUtils
const visibleMachineCustomFields = computed(() => {
const fields = Array.isArray(machineCustomFields.value) ? machineCustomFields.value : []
if (isEditMode.value) {
return fields
}
return fields.filter((field) => shouldDisplayCustomField(field))
})
// summarizeCustomFields → imported from customFieldUtils
// extractDefinitionName, extractDefinitionType, extractDefinitionRequired,
// extractOptionList, extractDefinitionOptions, extractDefinitionDefaultValue,
// coerceValueForType, normalizeExistingCustomFieldDefinitions,
// normalizeCustomFieldDefinitionEntry
// → all imported from customFieldUtils
// extractDefinitionRequired → imported from customFieldUtils
// extractOptionList → imported from customFieldUtils
// extractDefinitionOptions → imported from customFieldUtils
// extractDefinitionDefaultValue → imported from customFieldUtils
// coerceValueForType → imported from customFieldUtils
// normalizeExistingCustomFieldDefinitions → imported from customFieldUtils
// normalizeCustomFieldDefinitionEntry → imported from customFieldUtils
const getStructureCustomFields = (structure) => {
if (!structure || typeof structure !== 'object') {
return []
}
const normalized = normalizeStructureForEditor(structure)
return Array.isArray(normalized.customFields) ? normalized.customFields : []
}
// normalizeCustomFieldValueEntry → imported from customFieldUtils
// mergeCustomFieldValuesWithDefinitions → imported from customFieldUtils
// dedupeCustomFieldEntries → imported from customFieldUtils
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
}
// mergePieceLists, mergeComponentTrees → imported from useMachineHierarchy
// buildMachineHierarchyFromLinks → imported from useMachineHierarchy
// resolveLinkArray → imported from useMachineHierarchy
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, findProductById, constructeurs.value)
components.value = transformComponentCustomFields(hierarchy)
pieces.value = transformCustomFields(machineLevelPieces)
return true
}
// Methods
const loadMachineData = async () => {
loading.value = true
try {
const machineResult = await get(`/machines/${machineId}/skeleton`)
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>