Files
Inventory_frontend/app/pages/machine/[id].vue

3120 lines
106 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 class="badge badge-outline">{{ machine.typeMachine?.category || 'N/A' }}</div>
<div 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 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 || 'Non définie' }}
</div>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Constructeur</span>
</label>
<ConstructeurSelect
v-if="isEditMode"
class="w-full"
:key="machine.value?.id"
:model-value="machineConstructeurId"
placeholder="Rechercher un constructeur..."
@update:modelValue="handleMachineConstructeurChange"
/>
<div v-else class="input input-bordered bg-base-200">
<div class="flex flex-col">
<span class="font-medium">{{ machineConstructeurDisplay?.name || 'Non défini' }}</span>
<span class="text-xs text-gray-500">
{{ [machineConstructeurDisplay?.email, machineConstructeurDisplay?.phone].filter(Boolean).join(' • ') || '' }}
</span>
</div>
</div>
</div>
</div>
<!-- Champs personnalisés -->
<div v-if="machineCustomFields.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 machineCustomFields"
: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">
<span class="text-xl" :class="documentIcon(document).colorClass">
<component
:is="documentIcon(document).component"
class="h-6 w-6"
aria-hidden="true"
/>
</span>
<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>
<!-- Requirement Summary -->
<div
v-if="componentRequirementGroups.length || pieceRequirementGroups.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.composantModel" class="badge badge-sm badge-primary badge-outline">
Modèle : {{ component.composantModel.name }}
</span>
<span v-else class="badge badge-sm badge-outline">Défini manuellement</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>
</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.pieceModel" class="badge badge-sm badge-primary badge-outline">
Modèle : {{ piece.pieceModel.name }}
</span>
<span v-else class="badge badge-sm badge-outline">Définie manuellement</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>
</div>
<p v-else class="text-xs text-gray-500">Aucune pièce rattachée à ce groupe.</p>
</div>
</div>
</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"
:component-model-options-provider="getComponentModelOptions"
:piece-model-options-provider="getPieceModelOptions"
@update="updateComponent"
@edit-piece="updatePieceFromComponent"
@assign-model="assignComponentModel"
@assign-piece-model="assignPieceModel"
@custom-field-update="updatePieceCustomField"
@create-model-from-component="openSaveComponentModelModal"
/>
</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"
:piece-model-options="getPieceModelOptions(piece)"
@update="updatePieceInfo"
@edit="editPiece"
@custom-field-update="updatePieceCustomField"
@assign-model="assignPieceModel"
/>
</div>
</div>
</div>
</template>
<template v-else>
<div class="card bg-base-100 shadow-lg border border-primary/20">
<div class="card-body space-y-6">
<div class="flex flex-wrap items-start justify-between gap-3">
<div>
<h2 class="card-title">
{{ skeletonEditorTitle }}
</h2>
<p class="text-sm text-gray-500">
Sélectionnez les composants et pièces à associer à cette machine selon les exigences du type.
</p>
</div>
<button
type="button"
class="btn btn-ghost btn-sm"
:disabled="skeletonEditor.submitting"
@click="changeMachineView('details')"
>
Fermer
</button>
</div>
<div v-if="skeletonEditor.loading" class="flex justify-center py-12">
<span class="loading loading-spinner loading-lg"></span>
</div>
<template v-else>
<div v-if="!machineHasSkeletonRequirements" class="text-sm text-gray-500">
Ce type de machine ne possède pas de squelette structuré.
</div>
<template v-else>
<div v-if="componentRequirements.length" class="space-y-4">
<h3 class="text-sm font-semibold text-gray-700">Sélection des composants</h3>
<div
v-for="requirement in componentRequirements"
:key="requirement.id"
class="border border-base-200 rounded-lg p-4 space-y-3"
:id="`component-reconfigure-${requirement.id}`"
>
<div class="flex flex-wrap items-start justify-between gap-3">
<div>
<h4 class="font-medium text-sm">
{{ requirement.label || requirement.typeComposant?.name || 'Famille de composants' }}
</h4>
<p class="text-xs text-gray-500">
Type : {{ requirement.typeComposant?.name || 'Non défini' }} · Min : {{ requirement.minCount ?? (requirement.required ? 1 : 0) }}
· Max : {{ requirement.maxCount ?? '∞' }} ·
{{ requirement.allowNewModels ? 'Nouveaux modèles autorisés' : 'Modèles existants uniquement' }}
</p>
</div>
<button
type="button"
class="btn btn-sm btn-outline"
@click="addComponentSelectionEntry(requirement)"
:disabled="requirement.maxCount !== null && getComponentRequirementEntries(requirement.id).length >= requirement.maxCount"
>
<IconLucidePlus class="w-4 h-4 mr-2" aria-hidden="true" />
Ajouter
</button>
</div>
<div v-if="getComponentRequirementEntries(requirement.id).length === 0" class="text-xs text-gray-500">
Aucun composant sélectionné pour ce groupe.
</div>
<div
v-for="(entry, entryIndex) in getComponentRequirementEntries(requirement.id)"
:key="`${requirement.id}-${entryIndex}`"
class="bg-base-200/60 rounded-md p-3 space-y-3"
>
<div class="flex flex-wrap items-center gap-2 text-xs">
<label class="inline-flex items-center gap-1">
<input
type="radio"
class="radio radio-xs"
:checked="entry.mode === 'model'"
@change="setComponentSelectionMode(requirement.id, entryIndex, 'model')"
/>
Modèle existant
</label>
<label class="inline-flex items-center gap-1">
<input
type="radio"
class="radio radio-xs"
:checked="entry.mode === 'manual'"
@change="setComponentSelectionMode(requirement.id, entryIndex, 'manual')"
:disabled="!requirement.allowNewModels"
/>
Définir manuellement
</label>
</div>
<div v-if="entry.mode === 'model'" class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<div class="form-control">
<label class="label">
<span class="label-text text-xs">Modèle de composant</span>
</label>
<select
class="select select-bordered select-sm"
:value="entry.componentModelId || ''"
@change="handleRequirementComponentModelChange(requirement, entryIndex, $event.target.value || '')"
>
<option value="">Sélectionner un modèle</option>
<option
v-for="model in getComponentModelsForType(requirement.typeComposantId)"
:key="model.id"
:value="model.id"
>
{{ model.name }}
</option>
</select>
<p v-if="loadingComponentModels" class="text-[10px] text-gray-500 mt-1">Chargement des modèles...</p>
<p
v-else-if="getComponentModelsForType(requirement.typeComposantId).length === 0"
class="text-[10px] text-gray-500 mt-1"
>
Aucun modèle disponible pour ce type.
</p>
</div>
</div>
<SkeletonComponentNodeSelector
v-if="entry.definition && entry.componentModelId"
:node="entry.definition"
:show-self-selector="false"
:get-component-models-for-type="getComponentModelsForType"
:get-piece-models-for-type="getPieceModelsForType"
:loading-component-models="loadingComponentModels"
:loading-piece-models="loadingPieceModels"
@component-model-change="handleNodeComponentModelChange"
@piece-model-change="handleNodePieceModelChange"
/>
</div>
<div v-else class="grid grid-cols-1 md:grid-cols-2 gap-3">
<div class="form-control">
<label class="label">
<span class="label-text text-xs">Nom du composant</span>
</label>
<input
type="text"
class="input input-bordered input-sm"
:value="entry.name"
placeholder="Nom du composant"
@input="updateComponentSelectionEntry(requirement.id, entryIndex, { name: $event.target.value })"
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text text-xs">Référence (optionnel)</span>
</label>
<input
type="text"
class="input input-bordered input-sm"
placeholder="(Non géré pour l'instant)"
disabled
/>
</div>
</div>
<div class="flex justify-end">
<button
type="button"
class="btn btn-square btn-xs btn-error"
@click="removeComponentSelectionEntry(requirement.id, entryIndex)"
>
<IconLucideX class="w-4 h-4" aria-hidden="true" />
</button>
</div>
</div>
</div>
</div>
<div v-if="pieceRequirements.length" class="space-y-4">
<h3 class="text-sm font-semibold text-gray-700">Sélection des pièces principales</h3>
<div
v-for="requirement in pieceRequirements"
:key="requirement.id"
class="border border-base-200 rounded-lg p-4 space-y-3"
:id="`piece-reconfigure-${requirement.id}`"
>
<div class="flex flex-wrap items-start justify-between gap-3">
<div>
<h4 class="font-medium text-sm">
{{ requirement.label || requirement.typePiece?.name || 'Groupe de pièces' }}
</h4>
<p class="text-xs text-gray-500">
Type : {{ requirement.typePiece?.name || 'Non défini' }} · Min : {{ requirement.minCount ?? (requirement.required ? 1 : 0) }}
· Max : {{ requirement.maxCount ?? '∞' }} ·
{{ requirement.allowNewModels ? 'Nouveaux modèles autorisés' : 'Modèles existants uniquement' }}
</p>
</div>
<button
type="button"
class="btn btn-sm btn-outline"
@click="addPieceSelectionEntry(requirement)"
:disabled="requirement.maxCount !== null && getPieceRequirementEntries(requirement.id).length >= requirement.maxCount"
>
<IconLucidePlus class="w-4 h-4 mr-2" aria-hidden="true" />
Ajouter
</button>
</div>
<div v-if="getPieceRequirementEntries(requirement.id).length === 0" class="text-xs text-gray-500">
Aucune pièce sélectionnée pour ce groupe.
</div>
<div
v-for="(entry, entryIndex) in getPieceRequirementEntries(requirement.id)"
:key="`${requirement.id}-piece-${entryIndex}`"
class="bg-base-200/60 rounded-md p-3 space-y-3"
>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<div class="form-control">
<label class="label">
<span class="label-text text-xs">Modèle de pièce</span>
</label>
<select
class="select select-bordered select-sm"
:value="entry.pieceModelId || ''"
@change="updatePieceSelectionEntry(requirement.id, entryIndex, { pieceModelId: $event.target.value || '' })"
>
<option value="">Sélectionner un modèle</option>
<option
v-for="model in getPieceModelsForType(requirement.typePieceId)"
:key="model.id"
:value="model.id"
>
{{ model.name }}
</option>
</select>
<p v-if="loadingPieceModels" class="text-[10px] text-gray-500 mt-1">Chargement des modèles...</p>
<p
v-else-if="getPieceModelsForType(requirement.typePieceId).length === 0"
class="text-[10px] text-gray-500 mt-1"
>
Aucun modèle disponible pour ce type.
</p>
<p
v-else-if="!entry.pieceModelId && entry.legacyName"
class="text-[10px] text-warning mt-1"
>
Ancienne pièce : {{ entry.legacyName }} sélectionner un modèle.
</p>
</div>
</div>
<div class="flex justify-end">
<button
type="button"
class="btn btn-square btn-xs btn-error"
@click="removePieceSelectionEntry(requirement.id, entryIndex)"
>
<IconLucideX class="w-4 h-4" aria-hidden="true" />
</button>
</div>
</div>
</div>
</div>
</template>
<div class="flex flex-wrap justify-end gap-2 pt-2 border-t border-base-200">
<button
type="button"
class="btn btn-ghost"
:disabled="skeletonEditor.submitting"
@click="changeMachineView('details')"
>
Annuler
</button>
<button
type="button"
class="btn btn-primary"
:class="{ loading: skeletonEditor.submitting }"
@click="saveSkeletonConfiguration"
>
Sauvegarder la configuration
</button>
</div>
</template>
</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)"
/>
<div v-if="saveComponentAsModelModal.open" class="modal modal-open">
<div class="modal-box max-w-2xl">
<h3 class="font-bold text-lg">
Enregistrer « {{ saveComponentAsModelModal.component?.name || 'Composant' }} » comme modèle
</h3>
<p class="text-xs text-gray-500 mb-4">
Le modèle sera associé au type {{ saveComponentAsModelModal.typeLabel || 'de composant' }} et
pourra être réutilisé lors de la configuration d'autres machines.
</p>
<form class="space-y-4" @submit.prevent="submitSaveComponentModelModal">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="form-control">
<label class="label"><span class="label-text">Nom du modèle</span></label>
<input
v-model="saveComponentAsModelModal.form.name"
type="text"
class="input input-bordered input-sm"
required
/>
</div>
<div class="form-control">
<label class="label"><span class="label-text">Type de composant</span></label>
<select
v-model="saveComponentAsModelModal.form.typeComposantId"
class="select select-bordered select-sm"
required
disabled
>
<option :value="saveComponentAsModelModal.form.typeComposantId">
{{ saveComponentAsModelModal.typeLabel || 'Type de composant' }}
</option>
</select>
</div>
</div>
<div class="form-control">
<label class="label"><span class="label-text">Description</span></label>
<textarea
v-model="saveComponentAsModelModal.form.description"
class="textarea textarea-bordered textarea-sm"
rows="3"
placeholder="Notes optionnelles"
></textarea>
</div>
<div class="bg-base-200/60 border border-base-200 rounded-lg p-3 space-y-3">
<div class="flex flex-wrap items-center gap-2 text-xs text-gray-500">
<span class="badge badge-outline badge-sm">{{ saveComponentStructureSummary }}</span>
</div>
<ModelStructureViewer :structure="saveComponentAsModelModal.structure" />
</div>
<div class="modal-action">
<button type="button" class="btn btn-outline" @click="closeSaveComponentModelModal">
Annuler
</button>
<button type="submit" class="btn btn-primary" :class="{ loading: saveComponentAsModelModal.submitting }">
Sauvegarder et assigner
</button>
</div>
</form>
</div>
</div>
</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 { 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 { canPreviewDocument } 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 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'
import ModelStructureViewer from '~/components/ModelStructureViewer.vue'
import SkeletonComponentNodeSelector from '~/components/SkeletonComponentNodeSelector.vue'
import { useComponentModels } from '~/composables/useComponentModels'
import { usePieceModels } from '~/composables/usePieceModels'
import {
defaultStructure,
extractStructureFromComponent,
formatStructurePreview,
cloneStructure,
} from '~/shared/modelUtils'
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 {
getComposantsByMachine,
updateComposant: updateComposantApi
} = useComposants()
const {
getPiecesByMachine,
updatePiece: updatePieceApi
} = usePieces()
const { upsertCustomFieldValue, updateCustomFieldValue: updateCustomFieldValueApi } = useCustomFields()
const {
uploadDocuments,
deleteDocument,
loadDocumentsByMachine,
loadDocumentsByComponent,
loadDocumentsByPiece
} = useDocuments()
const {
loadComponentModels,
getComponentModelsForType,
createComponentModel,
loadingComponentModels,
} = useComponentModels()
const {
loadPieceModels,
getPieceModelsForType,
loadingPieceModels,
} = usePieceModels()
const toast = useToast()
// Data
const loading = ref(true)
const machine = ref(null)
const components = ref([])
const pieces = ref([])
const printAreaRef = ref(null)
const { constructeurs, loadConstructeurs } = useConstructeurs()
// Champs de la machine
const machineName = ref('')
const machineReference = ref('')
const machineConstructeurId = ref(null)
const machineConstructeurDisplay = computed(() => {
const id = machineConstructeurId.value || machine.value?.constructeur?.id || machine.value?.constructeurId
if (!id) return machine.value?.constructeur || null
return constructeurs.value.find(item => item.id === id) || machine.value?.constructeur || null
})
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 saveComponentAsModelModal = reactive({
open: false,
submitting: false,
component: null,
typeLabel: '',
structure: defaultStructure(),
form: {
name: '',
description: '',
typeComposantId: '',
},
})
const saveComponentStructureSummary = computed(() =>
formatStructurePreview(saveComponentAsModelModal.structure)
)
const skeletonEditor = reactive({
open: false,
loading: false,
submitting: false,
})
const componentRequirementSelections = reactive({})
const pieceRequirementSelections = reactive({})
const machineType = computed(() => machine.value?.typeMachine || null)
const componentRequirements = computed(() => machineType.value?.componentRequirements || [])
const pieceRequirements = computed(() => machineType.value?.pieceRequirements || [])
const machineHasSkeletonRequirements = computed(() =>
componentRequirements.value.length > 0 || pieceRequirements.value.length > 0
)
const componentTypeLabelMap = computed(() => {
const map = new Map()
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()
pieceRequirements.value.forEach((requirement) => {
const type = requirement.typePiece
if (type?.id) {
map.set(type.id, type.name || '')
}
})
return map
})
const getComponentRequirementById = (id) =>
componentRequirements.value.find((requirement) => requirement.id === id) || null
const getPieceRequirementById = (id) =>
pieceRequirements.value.find((requirement) => requirement.id === id) || null
const getComponentTypeLabel = (typeId) => {
if (!typeId) return ''
return componentTypeLabelMap.value.get(typeId) || ''
}
const getPieceTypeLabel = (typeId) => {
if (!typeId) return ''
return pieceTypeLabelMap.value.get(typeId) || ''
}
const normalizePieceNode = (source, context = {}) => {
const name = source?.name || context.name || source?.typePieceLabel || source?.typePiece?.name || ''
const typePieceId = source?.typePieceId
|| source?.typePiece?.id
|| context.typePieceId
|| ''
const typePieceLabel = source?.typePieceLabel
|| source?.typePiece?.name
|| context.typePieceLabel
|| ''
return {
name,
typePieceId,
typePieceLabel,
__pieceModelId: source?.__pieceModelId
?? source?.pieceModelId
?? source?.pieceModel?.id
?? context.pieceModelId
?? null,
customFields: Array.isArray(source?.customFields)
? cloneStructure(source.customFields)
: [],
}
}
const normalizeComponentNode = (source, context = {}) => {
const isRoot = context.isRoot ?? false
const typeComposantId = source?.typeComposantId
|| source?.typeComposant?.id
|| context.typeComposantId
|| ''
const node = {
name: source?.name || context.name || '' ,
description: source?.description || context.description || '',
typeComposantId,
typeComposantLabel: source?.typeComposantLabel
|| source?.typeComposant?.name
|| context.typeComposantLabel
|| getComponentTypeLabel(typeComposantId),
__componentModelId: source?.__componentModelId
?? source?.componentModelId
?? source?.composantModelId
?? source?.composantModel?.id
?? context.componentModelId
?? null,
__requirementId: source?.__requirementId ?? context.requirementId ?? null,
customFields: isRoot && Array.isArray(source?.customFields)
? cloneStructure(source.customFields)
: [],
pieces: isRoot && Array.isArray(source?.pieces)
? source.pieces.map((piece) => normalizePieceNode(piece))
: [],
subComponents: Array.isArray(source?.subComponents || source?.sousComposants)
? (source.subComponents || source.sousComposants).map((sub) => normalizeComponentNode(sub, {
name: sub?.name || '',
description: sub?.description || '',
typeComposantId: sub?.typeComposantId || sub?.typeComposant?.id || '',
typeComposantLabel: sub?.typeComposantLabel || sub?.typeComposant?.name || '',
componentModelId: sub?.__componentModelId
?? sub?.componentModelId
?? sub?.composantModelId
?? sub?.composantModel?.id
?? null,
requirementId: sub?.__requirementId ?? null,
isRoot: false,
}))
: [],
}
if (!node.name) {
node.name = node.typeComposantLabel || 'Composant'
}
return node
}
const gatherTypeIdsFromComponentNode = (node, componentTypeIds, pieceTypeIds) => {
if (!node || typeof node !== 'object') {
return
}
if (node.typeComposantId) {
componentTypeIds.add(node.typeComposantId)
}
if (Array.isArray(node.pieces)) {
node.pieces.forEach((piece) => {
const typeId = piece?.typePieceId
|| piece?.typePiece?.id
|| null
if (typeId) {
pieceTypeIds.add(typeId)
}
})
}
if (Array.isArray(node.subComponents)) {
node.subComponents.forEach((subNode) => {
gatherTypeIdsFromComponentNode(subNode, componentTypeIds, pieceTypeIds)
})
}
}
const createEmptyComponentDefinition = (requirement) => {
return normalizeComponentNode(
{
name: requirement?.typeComposant?.name || 'Composant',
typeComposantId: requirement?.typeComposantId || '',
},
{
requirementId: requirement?.id || null,
isRoot: true,
},
)
}
const createDefinitionFromModel = (model, requirement) => {
const structure = cloneStructure(model?.structure || defaultStructure())
if (!structure.name) {
structure.name = model?.name || requirement?.typeComposant?.name || 'Composant'
}
if (!structure.typeComposantId) {
structure.typeComposantId = model?.typeComposantId || requirement?.typeComposantId || ''
}
const definition = normalizeComponentNode(structure, {
requirementId: requirement?.id || null,
componentModelId: model?.id || null,
isRoot: true,
})
definition.__componentModelId = model?.id || null
return definition
}
const buildDefinitionFromComponent = (component, requirement) => {
const definition = normalizeComponentNode(component, {
requirementId: requirement?.id || null,
componentModelId: component?.composantModelId
|| component?.composantModel?.id
|| null,
typeComposantId: requirement?.typeComposantId
|| component?.typeComposantId
|| component?.typeComposant?.id
|| '',
typeComposantLabel: requirement?.typeComposant?.name
|| component?.typeComposant?.name
|| '',
isRoot: true,
})
return definition
}
const applyDefinitionToNode = (target, definition) => {
target.name = definition.name
target.description = definition.description
target.typeComposantId = definition.typeComposantId
target.typeComposantLabel = definition.typeComposantLabel
target.__componentModelId = definition.__componentModelId ?? null
target.__requirementId = definition.__requirementId ?? null
target.customFields = Array.isArray(definition.customFields)
? cloneStructure(definition.customFields)
: []
target.pieces = Array.isArray(definition.pieces)
? definition.pieces.map((piece) => normalizePieceNode(piece))
: []
target.subComponents = Array.isArray(definition.subComponents)
? definition.subComponents.map((sub) => normalizeComponentNode(sub, { isRoot: false }))
: []
}
const handleNodeComponentModelChange = async (node, typeComposantId, modelId) => {
const requirement = node.__requirementId
? getComponentRequirementById(node.__requirementId)
: null
const previousModelId = node.__componentModelId || null
if (previousModelId === modelId) {
return
}
if (typeComposantId) {
await loadComponentModels(typeComposantId)
}
const models = typeComposantId ? getComponentModelsForType(typeComposantId) : []
const selectedModel = models.find((model) => model.id === modelId)
if (selectedModel) {
const definition = createDefinitionFromModel(selectedModel, requirement)
if (!definition.name && node.name) {
definition.name = node.name
}
applyDefinitionToNode(node, definition)
node.__componentModelId = selectedModel.id
await ensureModelsForNodeDefinition(node)
} else {
node.__componentModelId = null
node.customFields = []
node.pieces = []
node.subComponents = []
}
}
const handleNodePieceModelChange = async (pieceNode, typePieceId, modelId) => {
if (typePieceId) {
await loadPieceModels(typePieceId)
}
pieceNode.__pieceModelId = modelId || null
}
const ensureModelsForNodeDefinition = async (node) => {
if (!node) {
return
}
const componentTypeIds = new Set()
const pieceTypeIds = new Set()
gatherTypeIdsFromComponentNode(node, componentTypeIds, pieceTypeIds)
const loaders = [
...Array.from(componentTypeIds).filter(Boolean).map((id) => loadComponentModels(id)),
...Array.from(pieceTypeIds).filter(Boolean).map((id) => loadPieceModels(id)),
]
if (!loaders.length) {
return
}
await Promise.allSettled(loaders)
}
const getComponentRequirementEntries = (requirementId) => {
return componentRequirementSelections[requirementId] || []
}
const getPieceRequirementEntries = (requirementId) => {
return pieceRequirementSelections[requirementId] || []
}
const createComponentSelectionEntry = (requirement) => ({
mode: 'model',
componentModelId: '',
name: requirement?.typeComposant?.name || '',
definition: createEmptyComponentDefinition(requirement),
})
const createPieceSelectionEntry = () => ({
pieceModelId: '',
})
const resetSkeletonRequirementSelections = () => {
Object.keys(componentRequirementSelections).forEach((key) => {
delete componentRequirementSelections[key]
})
Object.keys(pieceRequirementSelections).forEach((key) => {
delete pieceRequirementSelections[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 setComponentSelectionMode = (requirementId, index, mode) => {
const entries = getComponentRequirementEntries(requirementId)
const requirement = getComponentRequirementById(requirementId)
componentRequirementSelections[requirementId] = entries.map((entry, i) => {
if (i !== index) return entry
if (mode === 'model') {
return {
...entry,
mode: 'model',
componentModelId: entry.componentModelId || '',
definition: entry.definition || createEmptyComponentDefinition(requirement),
}
}
return {
...entry,
mode: 'manual',
componentModelId: '',
definition: entry.definition || createEmptyComponentDefinition(requirement),
}
})
}
const updateComponentSelectionEntry = (requirementId, index, patch) => {
const entries = getComponentRequirementEntries(requirementId)
const requirement = getComponentRequirementById(requirementId)
componentRequirementSelections[requirementId] = entries.map((entry, i) => {
if (i !== index) {
return entry
}
const updated = { ...entry, ...patch }
if (Object.prototype.hasOwnProperty.call(patch, 'componentModelId')) {
const newModelId = patch.componentModelId || ''
const typeId = requirement?.typeComposantId || ''
const models = typeId ? getComponentModelsForType(typeId) : []
const selectedModel = models.find((model) => model.id === newModelId)
if (selectedModel) {
updated.definition = createDefinitionFromModel(selectedModel, requirement)
} else {
updated.definition = createEmptyComponentDefinition(requirement)
}
}
if (Object.prototype.hasOwnProperty.call(patch, 'name') && updated.definition) {
updated.definition = {
...updated.definition,
name: patch.name,
}
}
return updated
})
}
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()]
}
const removePieceSelectionEntry = (requirementId, index) => {
const entries = getPieceRequirementEntries(requirementId)
pieceRequirementSelections[requirementId] = entries.filter((_, i) => i !== index)
}
const updatePieceSelectionEntry = (requirementId, index, patch) => {
const entries = getPieceRequirementEntries(requirementId)
pieceRequirementSelections[requirementId] = entries.map((entry, i) => {
if (i !== index) return entry
const updated = { ...entry, ...patch }
if (Object.prototype.hasOwnProperty.call(patch, 'pieceModelId')) {
if (patch.pieceModelId) {
delete updated.legacyName
}
}
return updated
})
}
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
}
const componentTypeIds = new Set()
const pieceTypeIds = new Set()
;(type.componentRequirements || []).forEach((requirement) => {
if (requirement.typeComposantId) {
componentTypeIds.add(requirement.typeComposantId)
}
const existingComponents = flattenedComponents.value.filter(
(component) => component.typeMachineComponentRequirementId === requirement.id
)
const entries = existingComponents.map((component) => {
const definition = buildDefinitionFromComponent(component, requirement)
gatherTypeIdsFromComponentNode(definition, componentTypeIds, pieceTypeIds)
const modelId = component.composantModelId || component.composantModel?.id || null
if (modelId) {
return { mode: 'model', componentModelId: modelId, name: '', definition }
}
return {
mode: 'manual',
componentModelId: '',
name: component.name || '',
definition,
}
})
const min = requirement.minCount ?? (requirement.required ? 1 : 0)
while (entries.length < min) {
const entry = createComponentSelectionEntry(requirement)
if (entry.definition) {
gatherTypeIdsFromComponentNode(entry.definition, componentTypeIds, pieceTypeIds)
}
entries.push(entry)
}
componentRequirementSelections[requirement.id] = entries.length ? entries : []
})
const allPieces = collectPiecesForSkeleton()
;(type.pieceRequirements || []).forEach((requirement) => {
if (requirement.typePieceId) {
pieceTypeIds.add(requirement.typePieceId)
}
const existingPieces = allPieces.filter(
(piece) => piece.typeMachinePieceRequirementId === requirement.id
)
const entries = existingPieces.map((piece) => {
const modelId = piece.pieceModelId || piece.pieceModel?.id || null
if (modelId) {
return { pieceModelId: modelId }
}
return {
pieceModelId: '',
legacyName: piece.name || piece.reference || '',
}
})
const min = requirement.minCount ?? (requirement.required ? 1 : 0)
while (entries.length < min) {
entries.push(createPieceSelectionEntry())
}
pieceRequirementSelections[requirement.id] = entries.length ? entries : []
})
await Promise.all([
...Array.from(componentTypeIds).filter(Boolean).map((id) => loadComponentModels(id)),
...Array.from(pieceTypeIds).filter(Boolean).map((id) => loadPieceModels(id)),
])
} finally {
skeletonEditor.loading = false
}
}
const handleRequirementComponentModelChange = async (requirement, entryIndex, value) => {
if (!requirement) {
return
}
updateComponentSelectionEntry(requirement.id, entryIndex, { componentModelId: value })
await nextTick()
const entries = getComponentRequirementEntries(requirement.id)
const entry = entries[entryIndex]
if (value && entry?.definition) {
await ensureModelsForNodeDefinition(entry.definition)
}
}
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 validateSkeletonSelections = (type) => {
const errors = []
const componentSelectionsPayload = []
const pieceSelectionsPayload = []
for (const requirement of type.componentRequirements || []) {
const entries = getComponentRequirementEntries(requirement.id)
const usableEntries = entries.filter((entry) => {
if (entry.mode === 'model') {
return !!entry.componentModelId
}
return !!entry.name && entry.name.trim().length > 0
})
const min = requirement.minCount ?? (requirement.required ? 1 : 0)
const max = requirement.maxCount ?? null
if (usableEntries.length < min) {
errors.push(
`Le groupe "${requirement.label || requirement.typeComposant?.name || 'Composants'}" nécessite au moins ${min} élément(s).`
)
}
if (max !== null && usableEntries.length > max) {
errors.push(
`Le groupe "${requirement.label || requirement.typeComposant?.name || 'Composants'}" ne peut dépasser ${max} élément(s).`
)
}
if (!requirement.allowNewModels && usableEntries.some((entry) => entry.mode === 'manual')) {
errors.push(
`Le groupe "${requirement.label || requirement.typeComposant?.name || 'Composants'}" n'autorise que les modèles existants.`
)
}
usableEntries.forEach((entry) => {
if (entry.mode === 'model') {
const definitionPayload = serializeComponentNodeDefinition(entry.definition)
const payload = {
requirementId: requirement.id,
componentModelId: entry.componentModelId,
}
if (
definitionPayload &&
((definitionPayload.pieces && definitionPayload.pieces.length) ||
(definitionPayload.subComponents && definitionPayload.subComponents.length) ||
(definitionPayload.customFields && definitionPayload.customFields.length))
) {
payload.definition = definitionPayload
}
componentSelectionsPayload.push(payload)
} else {
const manualName = typeof entry.name === 'string' ? entry.name.trim() : ''
const definitionPayload = serializeComponentNodeDefinition(entry.definition) || {}
if (manualName && !definitionPayload.name) {
definitionPayload.name = manualName
}
componentSelectionsPayload.push({
requirementId: requirement.id,
definition: Object.keys(definitionPayload).length
? definitionPayload
: { name: manualName || requirement.typeComposant?.name || 'Composant' },
})
}
})
}
for (const requirement of type.pieceRequirements || []) {
const entries = getPieceRequirementEntries(requirement.id)
const usableEntries = entries.filter((entry) => !!entry.pieceModelId)
const min = requirement.minCount ?? (requirement.required ? 1 : 0)
const max = requirement.maxCount ?? null
if (usableEntries.length < min) {
errors.push(
`Le groupe "${requirement.label || requirement.typePiece?.name || 'Pièces'}" nécessite au moins ${min} élément(s).`
)
}
if (max !== null && usableEntries.length > max) {
errors.push(
`Le groupe "${requirement.label || requirement.typePiece?.name || 'Pièces'}" ne peut dépasser ${max} élément(s).`
)
}
usableEntries.forEach((entry) => {
pieceSelectionsPayload.push({
requirementId: requirement.id,
pieceModelId: entry.pieceModelId,
})
})
}
if (errors.length > 0) {
return { valid: false, error: errors[0] }
}
return {
valid: true,
componentSelections: componentSelectionsPayload,
pieceSelections: pieceSelectionsPayload,
}
}
const parseOptionsText = (text) => {
return text
.split(/\r?\n/)
.map((option) => option.trim())
.filter((option) => option.length > 0)
}
const serializeCustomFieldDefinition = (field) => {
if (!field || typeof field !== 'object') {
return null
}
const name = typeof field.name === 'string' ? field.name.trim() : ''
if (!name) {
return null
}
const type = typeof field.type === 'string' && field.type.trim().length
? field.type.trim()
: 'text'
const result = {
name,
type,
required: !!field.required,
}
let options = []
if (Array.isArray(field.options) && field.options.length) {
options = field.options
.map((option) => (typeof option === 'string' ? option.trim() : ''))
.filter((option) => option.length > 0)
} else if (type === 'select' && typeof field.optionsText === 'string') {
options = parseOptionsText(field.optionsText)
}
if (options.length) {
result.options = options
}
return result
}
const serializePieceNodeDefinition = (piece) => {
if (!piece || typeof piece !== 'object') {
return null
}
const payload = {}
const name = typeof piece.name === 'string' ? piece.name.trim() : ''
if (name) {
payload.name = name
}
const typePieceId = piece.typePieceId || piece.typePiece?.id || ''
if (typePieceId) {
payload.typePieceId = typePieceId
}
const typePieceLabel = typeof piece.typePieceLabel === 'string' ? piece.typePieceLabel.trim() : ''
if (typePieceLabel) {
payload.typePieceLabel = typePieceLabel
}
if (piece.__pieceModelId) {
payload.pieceModelId = piece.__pieceModelId
}
if (Array.isArray(piece.customFields) && piece.customFields.length) {
const customFields = piece.customFields
.map((field) => serializeCustomFieldDefinition(field))
.filter(Boolean)
if (customFields.length) {
payload.customFields = customFields
}
}
return Object.keys(payload).length > 0 ? payload : null
}
const serializeComponentNodeDefinition = (node) => {
if (!node || typeof node !== 'object') {
return null
}
const payload = {}
const name = typeof node.name === 'string' ? node.name.trim() : ''
if (name) {
payload.name = name
}
const description = typeof node.description === 'string' ? node.description.trim() : ''
if (description) {
payload.description = description
}
const typeComposantId = node.typeComposantId || node.typeComposant?.id || ''
if (typeComposantId) {
payload.typeComposantId = typeComposantId
}
const typeComposantLabel = typeof node.typeComposantLabel === 'string'
? node.typeComposantLabel.trim()
: ''
if (typeComposantLabel) {
payload.typeComposantLabel = typeComposantLabel
}
if (node.__componentModelId) {
payload.componentModelId = node.__componentModelId
}
if (Array.isArray(node.customFields) && node.customFields.length) {
const customFields = node.customFields
.map((field) => serializeCustomFieldDefinition(field))
.filter(Boolean)
if (customFields.length) {
payload.customFields = customFields
}
}
if (Array.isArray(node.pieces) && node.pieces.length) {
const pieces = node.pieces
.map((piece) => serializePieceNodeDefinition(piece))
.filter(Boolean)
if (pieces.length) {
payload.pieces = pieces
}
}
if (Array.isArray(node.subComponents) && node.subComponents.length) {
const subComponents = node.subComponents
.map((sub) => serializeComponentNodeDefinition(sub))
.filter(Boolean)
if (subComponents.length) {
payload.subComponents = subComponents
}
}
return Object.keys(payload).length > 0 ? payload : null
}
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 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)
}
await ensureModelsForExistingEntities()
}
const saveSkeletonConfiguration = async () => {
if (!machine.value?.id) {
return
}
const type = machineType.value
let payload = { componentSelections: [], pieceSelections: [] }
if (type && machineHasSkeletonRequirements.value) {
const validation = validateSkeletonSelections(type)
if (!validation.valid) {
toast.showError(validation.error)
return
}
payload = {
componentSelections: validation.componentSelections,
pieceSelections: validation.pieceSelections,
}
}
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) => {
machineConstructeurId.value = value
await updateMachineInfo()
}
const openSaveComponentModelModal = (component) => {
if (!component) return
const requirement = component.typeMachineComponentRequirement || null
const typeComposantId = requirement?.typeComposantId
|| component.typeComposantId
|| component.typeComposant?.id
|| ''
saveComponentAsModelModal.open = true
saveComponentAsModelModal.submitting = false
saveComponentAsModelModal.component = component
saveComponentAsModelModal.typeLabel = requirement?.typeComposant?.name
|| component.typeComposant?.name
|| 'Composant'
saveComponentAsModelModal.form = {
name: component.name || '',
description: component.description || '',
typeComposantId,
}
saveComponentAsModelModal.structure = extractStructureFromComponent(component)
}
const closeSaveComponentModelModal = () => {
saveComponentAsModelModal.open = false
saveComponentAsModelModal.component = null
saveComponentAsModelModal.typeLabel = ''
saveComponentAsModelModal.structure = defaultStructure()
saveComponentAsModelModal.form = {
name: '',
description: '',
typeComposantId: '',
}
}
// 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'
})
const skeletonEditorTitle = computed(() =>
skeletonEditor.open
? 'Modification des éléments du squelette'
: 'Éléments du squelette'
)
// 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 || ''
machineConstructeurId.value = machine.value.constructeurId || machine.value.constructeur?.id || null
}
}
// 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 => !piece.composantId)
console.log('machinePieces computed:', filteredPieces)
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 preloadModelsForTypeMachine = async (typeMachine) => {
if (!typeMachine) return
const componentTypeIds = new Set(
(typeMachine.componentRequirements || [])
.map((requirement) => requirement.typeComposantId)
.filter(Boolean),
)
const pieceTypeIds = new Set(
(typeMachine.pieceRequirements || [])
.map((requirement) => requirement.typePieceId)
.filter(Boolean),
)
await Promise.all([
...Array.from(componentTypeIds).map((id) => loadComponentModels(id)),
...Array.from(pieceTypeIds).map((id) => loadPieceModels(id)),
])
}
const ensureModelsForExistingEntities = async () => {
const componentTypeIds = new Set()
const gatherComponentTypes = (items = []) => {
items.forEach((item) => {
const typeId = item.typeMachineComponentRequirement?.typeComposantId
|| item.typeComposantId
|| item.typeComposant?.id
if (typeId) {
componentTypeIds.add(typeId)
}
if (item.subComponents?.length) {
gatherComponentTypes(item.subComponents)
}
})
}
gatherComponentTypes(components.value)
const pieceTypeIds = new Set()
pieces.value.forEach((piece) => {
const typeId = piece.typeMachinePieceRequirement?.typePieceId
|| piece.typePieceId
|| piece.typePiece?.id
if (typeId) {
pieceTypeIds.add(typeId)
}
})
await Promise.all([
...Array.from(componentTypeIds).map((id) => loadComponentModels(id)),
...Array.from(pieceTypeIds).map((id) => loadPieceModels(id)),
])
}
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)
}
})
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, parentComponentName: null })
})
// Pièces rattachées aux composants
flattenedComponents.value.forEach((component) => {
if (component.pieces && component.pieces.length) {
component.pieces.forEach((piece) => {
collected.push({ ...piece, parentComponentName: component.name })
})
}
})
return collected
}
collectPieces().forEach((piece) => {
const reqId = piece.typeMachinePieceRequirementId
if (reqId && map.has(reqId)) {
map.get(reqId).pieces.push(piece)
}
})
return groups
})
const getComponentModelOptions = (entity) => {
const requirement = entity?.typeMachineComponentRequirement
const typeId = requirement?.typeComposantId
|| entity?.typeComposantId
|| entity?.typeComposant?.id
if (!typeId) return []
return getComponentModelsForType(typeId)
}
const getPieceModelOptions = (entity) => {
const requirement = entity?.typeMachinePieceRequirement
const typeId = requirement?.typePieceId
|| entity?.typePieceId
|| entity?.typePiece?.id
if (!typeId) return []
return getPieceModelsForType(typeId)
}
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 formatCustomFieldValue = (field) => {
if (!field) {
return 'Non défini'
}
const value = field.value ?? ''
if (!value && value !== 0) {
return 'Non défini'
}
if (field.type === 'boolean') {
const normalized = String(value).toLowerCase()
if (normalized === 'true') return 'Oui'
if (normalized === 'false') return 'Non'
}
return 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 summarizeCustomFields = (fields = []) => {
const seen = new Set()
return fields
.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 getStructureCustomFields = (structure) => {
if (!structure || typeof structure !== 'object') {
return []
}
const customFields = structure.customFields
return Array.isArray(customFields) ? customFields : []
}
// Transform custom field values to custom fields format
const mergeCustomFieldValuesWithDefinitions = (valueEntries = [], ...definitionSources) => {
const normalizedValues = (Array.isArray(valueEntries) ? valueEntries : []).map((entry) => {
const id = entry?.customField?.id ?? entry?.customFieldId ?? null
const name = entry?.customField?.name ?? ''
const type = entry?.customField?.type ?? 'text'
const required = !!entry?.customField?.required
const options = Array.isArray(entry?.customField?.options) ? entry.customField.options : []
return {
customFieldValueId: entry?.id ?? null,
id,
customFieldId: id,
name,
type,
required,
options,
value: entry?.value ?? '',
readOnly: false,
}
})
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 : []))
.filter(Boolean)
definitions.forEach((definition) => {
const normalizedDefinition = {
id: definition?.id ?? definition?.customFieldId ?? null,
name: definition?.name ?? '',
type: definition?.type ?? 'text',
required: !!definition?.required,
options: Array.isArray(definition?.options) ? definition.options : [],
customFieldId: definition?.id ?? definition?.customFieldId ?? null,
readOnly: !!definition?.readOnly,
}
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) {
existing.options = normalizedDefinition.options
}
existing.customFieldId = existing.customFieldId || normalizedDefinition.id
existing.readOnly = existing.readOnly && normalizedDefinition.readOnly
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,
value: '',
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 transformCustomFields = (pieces) => {
return (pieces || []).map((piece) => {
const customFields = mergeCustomFieldValuesWithDefinitions(
piece.customFieldValues,
piece.customFields,
piece.typePiece?.customFields,
piece.pieceModel?.customFields,
getStructureCustomFields(piece.pieceModel?.structure),
piece.typeMachinePieceRequirement?.customFields,
)
return {
...piece,
customFields,
documents: piece.documents || [],
constructeur: piece.constructeur || null,
constructeurId: piece.constructeurId || piece.constructeur?.id || null,
typePieceId: piece.typePieceId
|| piece.typeMachinePieceRequirement?.typePieceId
|| piece.typePiece?.id
|| null,
}
})
}
// Transform custom fields for components (now handles nested structure)
const transformComponentCustomFields = (componentsData) => {
return (componentsData || []).map((component) => {
const customFields = mergeCustomFieldValuesWithDefinitions(
component.customFieldValues,
component.customFields,
component.typeComposant?.customFields,
component.composantModel?.customFields,
getStructureCustomFields(component.composantModel?.structure),
component.typeMachineComponentRequirement?.customFields,
)
const pieces = component.pieces
? transformCustomFields(component.pieces).map((piece) => ({
...piece,
parentComponentName: component.name,
}))
: []
const subComponents = component.sousComposants
? transformComponentCustomFields(component.sousComposants)
: []
return {
...component,
customFields,
pieces,
subComponents,
documents: component.documents || [],
constructeur: component.constructeur || null,
constructeurId: component.constructeurId || component.constructeur?.id || null,
typeComposantId: component.typeComposantId
|| component.typeMachineComponentRequirement?.typeComposantId
|| component.typeComposant?.id
|| null,
}
})
}
const syncMachineCustomFields = () => {
if (!machine.value) {
machineCustomFields.value = []
return
}
const merged = mergeCustomFieldValuesWithDefinitions(
machine.value.customFieldValues,
machine.value.customFields,
machine.value.typeMachine?.customFields,
).map((field) => ({
...field,
readOnly: false,
}))
machineCustomFields.value = merged
}
function mergePieceLists(existing = [], updates = []) {
if (!existing.length) {
return updates
}
if (!updates.length) {
return existing
}
const updateMap = new Map(updates.map(piece => [piece.id, piece]))
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
}
if (!updates.length) {
return existing
}
const updateMap = new Map(updates.map(component => [component.id, component]))
const merged = existing.map(component => {
const update = updateMap.get(component.id)
if (!update) {
return component
}
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
}
// Methods
const loadMachineData = async () => {
loading.value = true
try {
console.log('Début du chargement des données pour machineId:', machineId)
// Load specific machine directly from API
const { apiCall } = useApi()
console.log('Appel API pour machine:', machineId)
const machineResult = await apiCall(`/machines/${machineId}`, { method: 'GET' })
console.log('Résultat machine complet:', machineResult)
console.log('machineResult.success:', machineResult.success)
console.log('machineResult.data:', machineResult.data)
console.log('machineResult.error:', machineResult.error)
console.log('Machine customFieldValues:', machineResult.data?.customFieldValues)
console.log('Nombre de champs personnalisés:', machineResult.data?.customFieldValues?.length)
if (machineResult.success) {
machine.value = machineResult.data
machine.value.documents = machine.value.documents || []
machine.value.customFieldValues = machine.value.customFieldValues || []
machineDocumentsLoaded.value = !!(machine.value.documents?.length)
console.log('Machine trouvée et assignée:', machine.value)
syncMachineCustomFields()
await preloadModelsForTypeMachine(machine.value.typeMachine)
} else {
console.error('Machine non trouvée:', machineId)
console.error('Erreur API:', machineResult.error)
loading.value = false
return
}
// Initialize machine fields
initMachineFields()
console.log('Champs machine initialisés')
console.log('Machine après initialisation:', machine.value)
console.log('Machine name:', machineName.value)
console.log('Machine reference:', machineReference.value)
// Load components with hierarchy
console.log('Chargement des composants avec hiérarchie...')
const componentsResult = await apiCall(`/composants/hierarchy/${machineId}`, { method: 'GET' })
console.log('Résultat composants:', componentsResult)
console.log('Structure des données reçues:', JSON.stringify(componentsResult.data, null, 2))
if (componentsResult.success) {
console.log('Transformation des composants...')
components.value = transformComponentCustomFields(componentsResult.data)
console.log('Composants chargés:', components.value.length)
console.log('Composants transformés:', components.value)
collapseAllComponents()
// Debug: afficher la hiérarchie
console.log('=== HIÉRARCHIE DES COMPOSANTS ===')
components.value.forEach(comp => {
console.log(`Composant: ${comp.name} (ID: ${comp.id}, Parent: ${comp.parentComposantId})`)
if (comp.subComponents && comp.subComponents.length > 0) {
console.log(` Sous-composants: ${comp.subComponents.map(sc => sc.name).join(', ')}`)
}
})
console.log('=== FIN HIÉRARCHIE ===')
await ensureModelsForExistingEntities()
} else {
console.error('Erreur lors du chargement des composants:', componentsResult.error)
}
// Load pieces from machine response instead of separate API call
if (machine.value && machine.value.pieces) {
// Transformer les champs personnalisés
pieces.value = transformCustomFields(machine.value.pieces)
console.log('Pièces transformées:', pieces.value)
console.log('Pièces chargées:', pieces.value.length)
await ensureModelsForExistingEntities()
} else {
console.log('Aucune pièce trouvée dans la réponse de la machine')
}
if (!machineDocumentsLoaded.value) {
await refreshMachineDocuments()
}
console.log('Chargement terminé avec succès')
} catch (error) {
console.error('Erreur lors du chargement des données:', error)
} finally {
loading.value = false
console.log('Loading terminé, loading.value =', loading.value)
}
}
const updateMachineInfo = async () => {
if (!machine.value) return
try {
const result = await updateMachineApi(machine.value.id, {
name: machineName.value,
reference: machineReference.value,
constructeurId: machineConstructeurId.value || null
})
if (result.success) {
machine.value = result.data
machineConstructeurId.value = result.data.constructeurId || result.data.constructeur?.id || null
}
} catch (error) {
console.error('Erreur lors de la mise à jour de la machine:', error)
}
}
const updateComponent = async (updatedComponent) => {
try {
const prixValue = updatedComponent.prix
const result = await updateComposantApi(updatedComponent.id, {
name: updatedComponent.name,
reference: updatedComponent.reference,
constructeurId: updatedComponent.constructeurId || updatedComponent.constructeur?.id || null,
prix: prixValue && prixValue !== '' ? parseFloat(prixValue) : null,
composantModelId: updatedComponent.composantModelId || updatedComponent.composantModel?.id || null,
})
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 result = await updatePieceApi(updatedPiece.id, {
name: updatedPiece.name,
reference: updatedPiece.reference,
constructeurId: updatedPiece.constructeurId || updatedPiece.constructeur?.id || null,
prix: updatedPiece.prix && updatedPiece.prix !== '' ? parseFloat(updatedPiece.prix) : null,
pieceModelId: updatedPiece.pieceModelId || updatedPiece.pieceModel?.id || null,
})
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 result = await updatePieceApi(updatedPiece.id, {
name: updatedPiece.name,
reference: updatedPiece.reference,
constructeurId: updatedPiece.constructeurId || updatedPiece.constructeur?.id || null,
prix: updatedPiece.prix && updatedPiece.prix !== '' ? parseFloat(updatedPiece.prix) : null,
pieceModelId: updatedPiece.pieceModelId || updatedPiece.pieceModel?.id || null,
})
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)
}
}
const assignComponentModel = async ({ componentId, composantModelId, previousModelId, previousModel }) => {
if (!componentId) return
try {
const result = await updateComposantApi(componentId, {
composantModelId: composantModelId || null,
})
if (result.success) {
const transformed = transformComponentCustomFields([result.data])[0]
const target = findComponentById(components.value, componentId)
if (target) {
Object.assign(target, transformed)
components.value = [...components.value]
}
} else {
const target = findComponentById(components.value, componentId)
if (target) {
target.composantModelId = previousModelId || null
target.composantModel = previousModel || null
components.value = [...components.value]
}
toast.showError(result.error || 'Impossible de mettre à jour le modèle du composant')
}
} catch (error) {
console.error('Erreur lors de l\'assignation du modèle de composant:', error)
const target = findComponentById(components.value, componentId)
if (target) {
target.composantModelId = previousModelId || null
target.composantModel = previousModel || null
components.value = [...components.value]
}
toast.showError('Impossible de mettre à jour le modèle du composant')
}
}
const submitSaveComponentModelModal = async () => {
if (!saveComponentAsModelModal.form.typeComposantId) {
toast.showError('Type de composant manquant pour créer le modèle')
return
}
if (!saveComponentAsModelModal.form.name.trim()) {
toast.showError('Veuillez renseigner un nom de modèle')
return
}
saveComponentAsModelModal.submitting = true
try {
const payload = {
name: saveComponentAsModelModal.form.name.trim(),
description: saveComponentAsModelModal.form.description.trim() || undefined,
typeComposantId: saveComponentAsModelModal.form.typeComposantId,
structure: saveComponentAsModelModal.structure,
}
const result = await createComponentModel(payload)
if (!result.success) {
toast.showError(result.error || 'Impossible de créer le modèle de composant')
return
}
const newModel = result.data
if (newModel?.typeComposantId) {
await loadComponentModels(newModel.typeComposantId)
}
const sourceComponent = saveComponentAsModelModal.component
if (sourceComponent && newModel?.id) {
await assignComponentModel({
componentId: sourceComponent.id,
composantModelId: newModel.id,
previousModelId: sourceComponent.composantModelId,
previousModel: sourceComponent.composantModel,
})
}
closeSaveComponentModelModal()
toast.showSuccess('Composant enregistré comme modèle')
} catch (error) {
console.error('Erreur lors de la création du modèle de composant:', error)
toast.showError('Impossible de créer le modèle de composant')
} finally {
saveComponentAsModelModal.submitting = false
}
}
const assignPieceModel = async ({ pieceId, pieceModelId, previousModelId, previousModel }) => {
if (!pieceId) return
try {
const result = await updatePieceApi(pieceId, {
pieceModelId: pieceModelId || null,
})
if (result.success) {
const transformed = transformCustomFields([result.data])[0]
const target = findPieceById(pieceId)
if (target) {
Object.assign(target, transformed)
pieces.value = [...pieces.value]
components.value = [...components.value]
}
} else {
const target = findPieceById(pieceId)
if (target) {
target.pieceModelId = previousModelId || null
target.pieceModel = previousModel || null
pieces.value = [...pieces.value]
components.value = [...components.value]
}
toast.showError(result.error || 'Impossible de mettre à jour le modèle de pièce')
}
} catch (error) {
console.error('Erreur lors de l\'assignation du modèle de pièce:', error)
const target = findPieceById(pieceId)
if (target) {
target.pieceModelId = previousModelId || null
target.pieceModel = previousModel || null
pieces.value = [...pieces.value]
components.value = [...components.value]
}
toast.showError('Impossible de mettre à jour le modèle de pièce')
}
}
// 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 = (component) => {
// TODO: Implement edit modal
console.log('Edit component:', component)
}
const editPiece = (piece) => {
// TODO: Implement edit modal
console.log('Edit piece:', piece)
}
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()
}
// Vérifier si on doit activer le mode édition depuis l'URL
const route = useRoute()
if (route.query.edit === 'true') {
isEditMode.value = true
console.log('Mode édition activé depuis l\'URL')
}
})
</script>