2274 lines
83 KiB
Vue
2274 lines
83 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-center md:justify-between">
|
|
<h1 class="text-3xl font-bold">Détails de la machine</h1>
|
|
<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="machineHasSkeletonRequirements"
|
|
type="button"
|
|
class="btn btn-outline"
|
|
:disabled="skeletonEditor.open"
|
|
@click="openSkeletonEditor"
|
|
>
|
|
Modifier les éléments du squelette
|
|
</button>
|
|
<button
|
|
v-if="!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>
|
|
|
|
<!-- 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>
|
|
|
|
<div
|
|
v-if="skeletonEditor.open"
|
|
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">Modifier les éléments du squelette</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="closeSkeletonEditor"
|
|
>
|
|
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="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="updateComponentSelectionEntry(requirement.id, entryIndex, { componentModelId: $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>
|
|
|
|
<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="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="setPieceSelectionMode(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="setPieceSelectionMode(requirement.id, entryIndex, 'manual')"
|
|
:disabled="!requirement.allowNewModels"
|
|
/>
|
|
Définir manuellement
|
|
</label>
|
|
</div>
|
|
|
|
<div v-if="entry.mode === 'model'" 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>
|
|
</div>
|
|
</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 de la pièce</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
class="input input-bordered input-sm"
|
|
:value="entry.name"
|
|
placeholder="Nom de la pièce"
|
|
@input="updatePieceSelectionEntry(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="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="closeSkeletonEditor"
|
|
>
|
|
Annuler
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="btn btn-primary"
|
|
:class="{ loading: skeletonEditor.submitting }"
|
|
@click="saveSkeletonConfiguration"
|
|
>
|
|
Sauvegarder la configuration
|
|
</button>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 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">Emplacement</span>
|
|
</label>
|
|
<input
|
|
v-if="isEditMode"
|
|
:id="getMachineFieldId('emplacement')"
|
|
v-model="machineEmplacement"
|
|
type="text"
|
|
class="input input-bordered"
|
|
@blur="updateMachineInfo"
|
|
/>
|
|
<div v-else class="input input-bordered bg-base-200">
|
|
{{ machineEmplacement || 'Non défini' }}
|
|
</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="machine && machine.customFieldValues && machine.customFieldValues.length > 0" 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="fieldValue in machine.customFieldValues"
|
|
:key="fieldValue.id"
|
|
class="form-control"
|
|
>
|
|
<label class="label">
|
|
<span class="label-text text-sm">{{ fieldValue.customField.name }}</span>
|
|
<span v-if="fieldValue.customField.required" class="label-text-alt text-error">*</span>
|
|
</label>
|
|
|
|
<template v-if="isEditMode">
|
|
<input
|
|
v-if="fieldValue.customField.type === 'text'"
|
|
:value="fieldValue.value"
|
|
@input="setMachineCustomFieldValue(fieldValue.id, $event.target.value)"
|
|
type="text"
|
|
:placeholder="fieldValue.customField.defaultValue || ''"
|
|
class="input input-bordered input-sm"
|
|
:required="fieldValue.customField.required"
|
|
@blur="updateMachineCustomField(fieldValue.id)"
|
|
/>
|
|
<input
|
|
v-else-if="fieldValue.customField.type === 'number'"
|
|
:value="fieldValue.value"
|
|
@input="setMachineCustomFieldValue(fieldValue.id, $event.target.value)"
|
|
type="number"
|
|
:placeholder="fieldValue.customField.defaultValue || ''"
|
|
class="input input-bordered input-sm"
|
|
:required="fieldValue.customField.required"
|
|
@blur="updateMachineCustomField(fieldValue.id)"
|
|
/>
|
|
<select
|
|
v-else-if="fieldValue.customField.type === 'select'"
|
|
:value="fieldValue.value"
|
|
@change="setMachineCustomFieldValue(fieldValue.id, $event.target.value)"
|
|
class="select select-bordered select-sm"
|
|
:required="fieldValue.customField.required"
|
|
@blur="updateMachineCustomField(fieldValue.id)"
|
|
>
|
|
<option value="">{{ fieldValue.customField.defaultValue || 'Sélectionner...' }}</option>
|
|
<option
|
|
v-for="option in fieldValue.customField.options"
|
|
:key="option"
|
|
:value="option"
|
|
>
|
|
{{ option }}
|
|
</option>
|
|
</select>
|
|
<div v-else-if="fieldValue.customField.type === 'boolean'" class="flex items-center gap-2">
|
|
<input
|
|
:value="fieldValue.value"
|
|
@change="setMachineCustomFieldValue(fieldValue.id, $event.target.checked ? 'true' : 'false')"
|
|
type="checkbox"
|
|
class="checkbox checkbox-sm"
|
|
:checked="fieldValue.value === 'true'"
|
|
@blur="updateMachineCustomField(fieldValue.id)"
|
|
/>
|
|
<span class="text-sm">{{ fieldValue.value === 'true' ? 'Oui' : 'Non' }}</span>
|
|
</div>
|
|
<input
|
|
v-else-if="fieldValue.customField.type === 'date'"
|
|
:value="fieldValue.value"
|
|
@input="setMachineCustomFieldValue(fieldValue.id, $event.target.value)"
|
|
type="date"
|
|
:placeholder="fieldValue.customField.defaultValue || ''"
|
|
class="input input-bordered input-sm"
|
|
:required="fieldValue.customField.required"
|
|
@blur="updateMachineCustomField(fieldValue.id)"
|
|
/>
|
|
</template>
|
|
<template v-else>
|
|
<div class="input input-bordered input-sm bg-base-200">
|
|
{{ fieldValue.value || fieldValue.customField.defaultValue || 'Non défini' }}
|
|
</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>
|
|
</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>
|
|
</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>
|
|
</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 { useComponentModels } from '~/composables/useComponentModels'
|
|
import { usePieceModels } from '~/composables/usePieceModels'
|
|
import {
|
|
defaultStructure,
|
|
extractStructureFromComponent,
|
|
formatStructurePreview,
|
|
} 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 } = 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 machineEmplacement = 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
|
|
})
|
|
|
|
// Valeurs des champs personnalisés de la machine
|
|
const machineCustomFieldValues = reactive({})
|
|
|
|
const machineDocumentFiles = ref([])
|
|
const machineDocumentsUploading = ref(false)
|
|
const machineDocumentsLoaded = ref(false)
|
|
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 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 getComponentRequirementEntries = (requirementId) => {
|
|
return componentRequirementSelections[requirementId] || []
|
|
}
|
|
|
|
const getPieceRequirementEntries = (requirementId) => {
|
|
return pieceRequirementSelections[requirementId] || []
|
|
}
|
|
|
|
const createComponentSelectionEntry = () => ({
|
|
mode: 'model',
|
|
componentModelId: '',
|
|
name: '',
|
|
})
|
|
|
|
const createPieceSelectionEntry = () => ({
|
|
mode: 'model',
|
|
pieceModelId: '',
|
|
name: '',
|
|
})
|
|
|
|
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()]
|
|
}
|
|
|
|
const removeComponentSelectionEntry = (requirementId, index) => {
|
|
const entries = getComponentRequirementEntries(requirementId)
|
|
componentRequirementSelections[requirementId] = entries.filter((_, i) => i !== index)
|
|
}
|
|
|
|
const setComponentSelectionMode = (requirementId, index, mode) => {
|
|
const entries = getComponentRequirementEntries(requirementId)
|
|
componentRequirementSelections[requirementId] = entries.map((entry, i) => {
|
|
if (i !== index) return entry
|
|
if (mode === 'model') {
|
|
return { ...entry, mode: 'model', componentModelId: entry.componentModelId || '', name: '' }
|
|
}
|
|
return { ...entry, mode: 'manual', componentModelId: '', name: entry.name || '' }
|
|
})
|
|
}
|
|
|
|
const updateComponentSelectionEntry = (requirementId, index, patch) => {
|
|
const entries = getComponentRequirementEntries(requirementId)
|
|
componentRequirementSelections[requirementId] = entries.map((entry, i) =>
|
|
i === index ? { ...entry, ...patch } : entry
|
|
)
|
|
}
|
|
|
|
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 setPieceSelectionMode = (requirementId, index, mode) => {
|
|
const entries = getPieceRequirementEntries(requirementId)
|
|
pieceRequirementSelections[requirementId] = entries.map((entry, i) => {
|
|
if (i !== index) return entry
|
|
if (mode === 'model') {
|
|
return { ...entry, mode: 'model', pieceModelId: entry.pieceModelId || '', name: '' }
|
|
}
|
|
return { ...entry, mode: 'manual', pieceModelId: '', name: entry.name || '' }
|
|
})
|
|
}
|
|
|
|
const updatePieceSelectionEntry = (requirementId, index, patch) => {
|
|
const entries = getPieceRequirementEntries(requirementId)
|
|
pieceRequirementSelections[requirementId] = entries.map((entry, i) =>
|
|
i === index ? { ...entry, ...patch } : entry
|
|
)
|
|
}
|
|
|
|
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()
|
|
;(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 modelId = component.composantModelId || component.composantModel?.id || null
|
|
if (modelId) {
|
|
return { mode: 'model', componentModelId: modelId, name: '' }
|
|
}
|
|
return { mode: 'manual', componentModelId: '', name: component.name || '' }
|
|
})
|
|
const min = requirement.minCount ?? (requirement.required ? 1 : 0)
|
|
while (entries.length < min) {
|
|
entries.push(createComponentSelectionEntry())
|
|
}
|
|
componentRequirementSelections[requirement.id] = entries.length ? entries : []
|
|
})
|
|
|
|
const pieceTypeIds = new Set()
|
|
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 { mode: 'model', pieceModelId: modelId, name: '' }
|
|
}
|
|
return { mode: 'manual', pieceModelId: '', name: piece.name || '' }
|
|
})
|
|
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 openSkeletonEditor = async () => {
|
|
if (skeletonEditor.open) {
|
|
return
|
|
}
|
|
skeletonEditor.open = true
|
|
await initializeSkeletonRequirementSelections()
|
|
}
|
|
|
|
const closeSkeletonEditor = () => {
|
|
if (skeletonEditor.submitting) {
|
|
return
|
|
}
|
|
skeletonEditor.open = false
|
|
skeletonEditor.loading = false
|
|
skeletonEditor.submitting = false
|
|
resetSkeletonRequirementSelections()
|
|
}
|
|
|
|
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') {
|
|
componentSelectionsPayload.push({
|
|
requirementId: requirement.id,
|
|
componentModelId: entry.componentModelId,
|
|
})
|
|
} else {
|
|
componentSelectionsPayload.push({
|
|
requirementId: requirement.id,
|
|
definition: {
|
|
name: entry.name.trim(),
|
|
},
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
for (const requirement of type.pieceRequirements || []) {
|
|
const entries = getPieceRequirementEntries(requirement.id)
|
|
const usableEntries = entries.filter((entry) => {
|
|
if (entry.mode === 'model') {
|
|
return !!entry.pieceModelId
|
|
}
|
|
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.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).`
|
|
)
|
|
}
|
|
|
|
if (!requirement.allowNewModels && usableEntries.some((entry) => entry.mode === 'manual')) {
|
|
errors.push(
|
|
`Le groupe "${requirement.label || requirement.typePiece?.name || 'Pièces'}" n'autorise que les modèles existants.`
|
|
)
|
|
}
|
|
|
|
usableEntries.forEach((entry) => {
|
|
if (entry.mode === 'model') {
|
|
pieceSelectionsPayload.push({
|
|
requirementId: requirement.id,
|
|
pieceModelId: entry.pieceModelId,
|
|
})
|
|
} else {
|
|
pieceSelectionsPayload.push({
|
|
requirementId: requirement.id,
|
|
definition: {
|
|
name: entry.name.trim(),
|
|
},
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
if (errors.length > 0) {
|
|
return { valid: false, error: errors[0] }
|
|
}
|
|
|
|
return {
|
|
valid: true,
|
|
componentSelections: componentSelectionsPayload,
|
|
pieceSelections: pieceSelectionsPayload,
|
|
}
|
|
}
|
|
|
|
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)
|
|
skeletonEditor.open = false
|
|
resetSkeletonRequirementSelections()
|
|
} 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
|
|
|
|
// 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 || ''
|
|
machineEmplacement.value = machine.value.emplacement || ''
|
|
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,
|
|
machineEmplacement: machineEmplacement.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]}`
|
|
}
|
|
|
|
// Transform custom field values to custom fields format
|
|
const transformCustomFields = (pieces) => {
|
|
return pieces.map(piece => {
|
|
const customFields = piece.customFieldValues?.map(cfv => ({
|
|
id: cfv.customField.id,
|
|
name: cfv.customField.name,
|
|
type: cfv.customField.type,
|
|
required: cfv.customField.required,
|
|
defaultValue: cfv.customField.defaultValue,
|
|
options: cfv.customField.options || [],
|
|
value: cfv.value
|
|
})) || []
|
|
|
|
return {
|
|
...piece,
|
|
customFields,
|
|
documents: piece.documents || [],
|
|
constructeur: piece.constructeur || null,
|
|
constructeurId: piece.constructeurId || piece.constructeur?.id || null,
|
|
}
|
|
})
|
|
}
|
|
|
|
// Transform custom fields for components (now handles nested structure)
|
|
const transformComponentCustomFields = (componentsData) => {
|
|
console.log('transformComponentCustomFields called with:', componentsData)
|
|
|
|
return componentsData.map(component => {
|
|
console.log('Processing component:', component.name, 'with sousComposants:', component.sousComposants?.length || 0)
|
|
|
|
// Transform custom fields for the current component
|
|
const customFields = component.customFieldValues?.map(cfv => ({
|
|
id: cfv.customField.id,
|
|
name: cfv.customField.name,
|
|
type: cfv.customField.type,
|
|
required: cfv.customField.required,
|
|
defaultValue: cfv.customField.defaultValue,
|
|
options: cfv.customField.options || [],
|
|
value: cfv.value
|
|
})) || [];
|
|
|
|
// Transform pieces for the current component
|
|
const pieces = component.pieces
|
|
? transformCustomFields(component.pieces).map((piece) => ({
|
|
...piece,
|
|
parentComponentName: component.name,
|
|
}))
|
|
: [];
|
|
|
|
// Recursively transform sub-components (using 'sousComposants' from backend)
|
|
const subComponents = component.sousComposants ? transformComponentCustomFields(component.sousComposants) : [];
|
|
|
|
const result = {
|
|
...component,
|
|
customFields, // Use customFields for frontend display
|
|
pieces,
|
|
subComponents, // Use the transformed sousComposants as subComponents
|
|
documents: component.documents || [],
|
|
constructeur: component.constructeur || null,
|
|
constructeurId: component.constructeurId || component.constructeur?.id || null,
|
|
};
|
|
|
|
console.log('Transformed component:', result.name, 'with subComponents:', result.subComponents?.length || 0)
|
|
return result;
|
|
});
|
|
};
|
|
|
|
// 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 || []
|
|
machineDocumentsLoaded.value = !!(machine.value.documents?.length)
|
|
console.log('Machine trouvée et assignée:', machine.value)
|
|
|
|
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,
|
|
emplacement: machineEmplacement.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,
|
|
emplacement: updatedComponent.emplacement,
|
|
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,
|
|
emplacement: updatedPiece.emplacement,
|
|
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,
|
|
emplacement: updatedPiece.emplacement,
|
|
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)
|
|
}
|
|
} else {
|
|
const target = findComponentById(components.value, componentId)
|
|
if (target) {
|
|
target.composantModelId = previousModelId || null
|
|
target.composantModel = previousModel || null
|
|
}
|
|
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
|
|
}
|
|
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)
|
|
}
|
|
} else {
|
|
const target = findPieceById(pieceId)
|
|
if (target) {
|
|
target.pieceModelId = previousModelId || null
|
|
target.pieceModel = previousModel || null
|
|
}
|
|
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
|
|
}
|
|
toast.showError('Impossible de mettre à jour le modèle de pièce')
|
|
}
|
|
}
|
|
|
|
// Méthodes pour les champs personnalisés de la machine
|
|
const setMachineCustomFieldValue = (fieldValueId, value) => {
|
|
const fieldValue = machine.value?.customFieldValues?.find(fv => fv.id === fieldValueId)
|
|
if (fieldValue) {
|
|
fieldValue.value = value
|
|
}
|
|
}
|
|
|
|
const updateMachineCustomField = async (fieldValueId) => {
|
|
const fieldValue = machine.value?.customFieldValues?.find(fv => fv.id === fieldValueId)
|
|
if (fieldValue) {
|
|
const { updateCustomFieldValue } = useCustomFields()
|
|
const { showSuccess, showError } = useToast()
|
|
|
|
const result = await updateCustomFieldValue(fieldValueId, { value: fieldValue.value })
|
|
if (result.success) {
|
|
showSuccess(`Champ "${fieldValue.customField.name}" de la machine mis à jour avec succès`)
|
|
} else {
|
|
showError(`Erreur lors de la mise à jour du champ "${fieldValue.customField.name}"`)
|
|
}
|
|
}
|
|
}
|
|
|
|
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(
|
|
() => [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>
|