feat: refactor type management screens
This commit is contained in:
@@ -2,156 +2,521 @@
|
||||
<div class="border border-gray-300 rounded-lg p-4 bg-base-100">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost btn-xs p-1"
|
||||
@click="toggleComponent"
|
||||
title="Plier / déplier le composant"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4 transition-transform duration-200"
|
||||
:class="{ 'rotate-90': expanded }"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<svg class="w-5 h-5 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path>
|
||||
</svg>
|
||||
<h4 class="text-lg font-medium">{{ component.name }}</h4>
|
||||
<span v-if="!expanded && component.reference" class="text-xs text-gray-500">(Ref: {{ component.reference }})</span>
|
||||
</div>
|
||||
<div class="text-sm text-gray-500">
|
||||
{{ component.reference ? `Ref: ${component.reference}` : '' }}
|
||||
<div v-if="!expanded && compactInfo" class="text-xs text-gray-500">
|
||||
{{ compactInfo }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Informations du composant -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<div v-if="component.prestataire" class="text-sm">
|
||||
<span class="font-medium">Prestataire:</span> {{ component.prestataire }}
|
||||
</div>
|
||||
<div v-if="component.emplacement" class="text-sm">
|
||||
<span class="font-medium">Emplacement:</span> {{ component.emplacement }}
|
||||
</div>
|
||||
<div v-if="component.prix" class="text-sm">
|
||||
<span class="font-medium">Prix:</span> {{ component.prix }}€
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Champs personnalisés du composant -->
|
||||
<div v-if="component.customFields && component.customFields.length > 0" class="mb-4">
|
||||
<h5 class="text-sm font-medium text-gray-700 mb-2">Champs personnalisés:</h5>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2">
|
||||
<div v-for="field in component.customFields" :key="field.id" class="bg-gray-50 rounded p-2">
|
||||
<div class="text-xs font-medium">{{ field.name }}</div>
|
||||
<div class="text-xs text-gray-600">{{ field.type }}</div>
|
||||
<div v-if="field.required" class="text-xs text-red-500">Obligatoire</div>
|
||||
<div v-if="field.defaultValue" class="text-xs text-gray-500">Défaut: {{ field.defaultValue }}</div>
|
||||
<template v-if="expanded">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<div v-if="component.prestataire" class="text-sm">
|
||||
<span class="font-medium">Prestataire:</span> {{ component.prestataire }}
|
||||
</div>
|
||||
<div v-if="component.emplacement" class="text-sm">
|
||||
<span class="font-medium">Emplacement:</span> {{ component.emplacement }}
|
||||
</div>
|
||||
<div v-if="component.prix" class="text-sm">
|
||||
<span class="font-medium">Prix:</span> {{ component.prix }}€
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pièces du composant -->
|
||||
<div v-if="component.pieces && component.pieces.length > 0" class="mb-4">
|
||||
<h5 class="text-sm font-medium text-gray-700 mb-2">Pièces du composant:</h5>
|
||||
<div class="space-y-2">
|
||||
<div v-for="(piece, pieceIndex) in component.pieces" :key="pieceIndex" class="border border-gray-200 rounded p-3 bg-gray-50">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"></path>
|
||||
</svg>
|
||||
<span class="font-medium">{{ piece.name }}</span>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500">
|
||||
{{ piece.reference ? `Ref: ${piece.reference}` : '' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Informations de la pièce -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-2 mb-2">
|
||||
<div v-if="piece.prestataire" class="text-xs">
|
||||
<span class="font-medium">Prestataire:</span> {{ piece.prestataire }}
|
||||
</div>
|
||||
<div v-if="piece.emplacement" class="text-xs">
|
||||
<span class="font-medium">Emplacement:</span> {{ piece.emplacement }}
|
||||
</div>
|
||||
<div v-if="piece.prix" class="text-xs">
|
||||
<span class="font-medium">Prix:</span> {{ piece.prix }}€
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Champs personnalisés de la pièce -->
|
||||
<div v-if="piece.customFields && piece.customFields.length > 0">
|
||||
<div class="text-xs font-medium text-gray-600 mb-1">Champs personnalisés:</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-1">
|
||||
<div v-for="field in piece.customFields" :key="field.id" class="bg-white rounded p-1">
|
||||
<div class="text-xs font-medium">{{ field.name }}</div>
|
||||
<div class="text-xs text-gray-600">{{ field.type }}</div>
|
||||
<div v-if="field.required" class="text-xs text-red-500">Obligatoire</div>
|
||||
<div v-if="field.defaultValue" class="text-xs text-gray-500">Défaut: {{ field.defaultValue }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="component.customFields && component.customFields.length > 0" class="mb-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h5 class="text-sm font-medium text-gray-700">Champs personnalisés</h5>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost btn-xs p-1"
|
||||
@click="toggleSection('customFields')"
|
||||
>
|
||||
<svg
|
||||
class="w-3 h-3 transition-transform duration-200"
|
||||
:class="{ 'rotate-90': expandedSections.customFields }"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="expandedSections.customFields" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2">
|
||||
<div v-for="field in component.customFields" :key="field.id || field.name" class="bg-gray-50 rounded p-2">
|
||||
<div class="text-xs font-medium">{{ field.name }}</div>
|
||||
<div class="text-xs text-gray-600">{{ field.type }}</div>
|
||||
<div v-if="field.required" class="text-xs text-red-500">Obligatoire</div>
|
||||
<div v-if="field.defaultValue" class="text-xs text-gray-500">Défaut: {{ field.defaultValue }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sous-composants -->
|
||||
<div v-if="component.subComponents && component.subComponents.length > 0" class="ml-4 border-l-2 border-gray-300 pl-4">
|
||||
<h5 class="text-sm font-medium text-gray-700 mb-2">Sous-composants:</h5>
|
||||
<div class="space-y-3">
|
||||
<div v-for="(subComponent, subIndex) in component.subComponents" :key="subIndex" class="border border-gray-200 rounded p-3 bg-gray-50">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<span class="font-medium">{{ subComponent.name }}</span>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500">
|
||||
{{ subComponent.reference ? `Ref: ${subComponent.reference}` : '' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Informations du sous-composant -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-2 mb-2">
|
||||
<div v-if="subComponent.prestataire" class="text-xs">
|
||||
<span class="font-medium">Prestataire:</span> {{ subComponent.prestataire }}
|
||||
</div>
|
||||
<div v-if="subComponent.emplacement" class="text-xs">
|
||||
<span class="font-medium">Emplacement:</span> {{ subComponent.emplacement }}
|
||||
</div>
|
||||
<div v-if="subComponent.prix" class="text-xs">
|
||||
<span class="font-medium">Prix:</span> {{ subComponent.prix }}€
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Champs personnalisés du sous-composant -->
|
||||
<div v-if="subComponent.customFields && subComponent.customFields.length > 0" class="mb-2">
|
||||
<div class="text-xs font-medium text-gray-600 mb-1">Champs personnalisés:</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-1">
|
||||
<div v-for="field in subComponent.customFields" :key="field.id" class="bg-white rounded p-1">
|
||||
<div class="text-xs font-medium">{{ field.name }}</div>
|
||||
<div class="text-xs text-gray-600">{{ field.type }}</div>
|
||||
<div v-if="field.required" class="text-xs text-red-500">Obligatoire</div>
|
||||
<div v-if="field.defaultValue" class="text-xs text-gray-500">Défaut: {{ field.defaultValue }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pièces du sous-composant -->
|
||||
<div v-if="subComponent.pieces && subComponent.pieces.length > 0">
|
||||
<div class="text-xs font-medium text-gray-600 mb-1">Pièces:</div>
|
||||
<div class="space-y-1">
|
||||
<div v-for="(piece, pieceIndex) in subComponent.pieces" :key="pieceIndex" class="flex items-center gap-2">
|
||||
<svg class="w-3 h-3 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<div v-if="component.pieces && component.pieces.length > 0" class="mb-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h5 class="text-sm font-medium text-gray-700">Pièces du composant</h5>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost btn-xs p-1"
|
||||
@click="toggleSection('pieces')"
|
||||
>
|
||||
<svg
|
||||
class="w-3 h-3 transition-transform duration-200"
|
||||
:class="{ 'rotate-90': expandedSections.pieces }"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="expandedSections.pieces" class="space-y-2">
|
||||
<div
|
||||
v-for="(piece, pieceIndex) in component.pieces"
|
||||
:key="pieceIndex"
|
||||
class="border border-gray-200 rounded p-3 bg-gray-50"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost btn-xs p-1"
|
||||
@click="togglePieceDetails(pieceIndex)"
|
||||
>
|
||||
<svg
|
||||
class="w-3 h-3 transition-transform duration-200"
|
||||
:class="{ 'rotate-90': isPieceExpanded(pieceIndex) }"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<svg class="w-4 h-4 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"></path>
|
||||
</svg>
|
||||
<span class="text-xs">{{ piece.name }}</span>
|
||||
<span v-if="piece.reference" class="text-xs text-gray-500">({{ piece.reference }})</span>
|
||||
<span v-if="piece.prix" class="text-xs text-green-600">{{ piece.prix }}€</span>
|
||||
<span class="font-medium">{{ piece.name }}</span>
|
||||
<span v-if="!isPieceExpanded(pieceIndex) && piece.reference" class="text-xs text-gray-500">(Ref: {{ piece.reference }})</span>
|
||||
</div>
|
||||
<div v-if="!isPieceExpanded(pieceIndex) && quickPieceSummary(piece)" class="text-xs text-gray-500">
|
||||
{{ quickPieceSummary(piece) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isPieceExpanded(pieceIndex)">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-2 mb-2">
|
||||
<div v-if="piece.prestataire" class="text-xs">
|
||||
<span class="font-medium">Prestataire:</span> {{ piece.prestataire }}
|
||||
</div>
|
||||
<div v-if="piece.emplacement" class="text-xs">
|
||||
<span class="font-medium">Emplacement:</span> {{ piece.emplacement }}
|
||||
</div>
|
||||
<div v-if="piece.prix" class="text-xs">
|
||||
<span class="font-medium">Prix:</span> {{ piece.prix }}€
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="piece.customFields && piece.customFields.length > 0">
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<span class="text-xs font-medium text-gray-600">Champs personnalisés</span>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost btn-xs p-1"
|
||||
@click="togglePieceCustomFields(pieceIndex)"
|
||||
>
|
||||
<svg
|
||||
class="w-3 h-3 transition-transform duration-200"
|
||||
:class="{ 'rotate-90': isPieceCustomFieldSectionExpanded(pieceIndex) }"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="isPieceCustomFieldSectionExpanded(pieceIndex)" class="grid grid-cols-1 md:grid-cols-2 gap-1">
|
||||
<div v-for="field in piece.customFields" :key="field.id || field.name" class="bg-white rounded p-1">
|
||||
<div class="text-xs font-medium">{{ field.name }}</div>
|
||||
<div class="text-xs text-gray-600">{{ field.type }}</div>
|
||||
<div v-if="field.required" class="text-xs text-red-500">Obligatoire</div>
|
||||
<div v-if="field.defaultValue" class="text-xs text-gray-500">Défaut: {{ field.defaultValue }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="component.subComponents && component.subComponents.length > 0" class="ml-4 border-l-2 border-gray-300 pl-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h5 class="text-sm font-medium text-gray-700">Sous-composants</h5>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost btn-xs p-1"
|
||||
@click="toggleSection('subComponents')"
|
||||
>
|
||||
<svg
|
||||
class="w-3 h-3 transition-transform duration-200"
|
||||
:class="{ 'rotate-90': expandedSections.subComponents }"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="expandedSections.subComponents" class="space-y-3">
|
||||
<div
|
||||
v-for="(subComponent, subIndex) in component.subComponents"
|
||||
:key="subIndex"
|
||||
class="border border-gray-200 rounded p-3 bg-gray-50"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost btn-xs p-1"
|
||||
@click="toggleSubComponentDetails(subIndex)"
|
||||
>
|
||||
<svg
|
||||
class="w-3 h-3 transition-transform duration-200"
|
||||
:class="{ 'rotate-90': isSubComponentExpanded(subIndex) }"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<svg class="w-4 h-4 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<span class="font-medium">{{ subComponent.name }}</span>
|
||||
<span v-if="!isSubComponentExpanded(subIndex) && subComponent.reference" class="text-xs text-gray-500">(Ref: {{ subComponent.reference }})</span>
|
||||
</div>
|
||||
<div v-if="!isSubComponentExpanded(subIndex) && quickSubSummary(subComponent)" class="text-xs text-gray-500">
|
||||
{{ quickSubSummary(subComponent) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isSubComponentExpanded(subIndex)" class="space-y-3">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||
<div v-if="subComponent.prestataire" class="text-xs">
|
||||
<span class="font-medium">Prestataire:</span> {{ subComponent.prestataire }}
|
||||
</div>
|
||||
<div v-if="subComponent.emplacement" class="text-xs">
|
||||
<span class="font-medium">Emplacement:</span> {{ subComponent.emplacement }}
|
||||
</div>
|
||||
<div v-if="subComponent.prix" class="text-xs">
|
||||
<span class="font-medium">Prix:</span> {{ subComponent.prix }}€
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="subComponent.customFields && subComponent.customFields.length > 0" class="mb-2">
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<span class="text-xs font-medium text-gray-600">Champs personnalisés</span>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost btn-xs p-1"
|
||||
@click="toggleSubComponentCustomFieldSection(subIndex)"
|
||||
>
|
||||
<svg
|
||||
class="w-3 h-3 transition-transform duration-200"
|
||||
:class="{ 'rotate-90': isSubComponentCustomFieldSectionExpanded(subIndex) }"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="isSubComponentCustomFieldSectionExpanded(subIndex)" class="grid grid-cols-1 md:grid-cols-2 gap-1">
|
||||
<div v-for="field in subComponent.customFields" :key="field.id || field.name" class="bg-white rounded p-1">
|
||||
<div class="text-xs font-medium">{{ field.name }}</div>
|
||||
<div class="text-xs text-gray-600">{{ field.type }}</div>
|
||||
<div v-if="field.required" class="text-xs text-red-500">Obligatoire</div>
|
||||
<div v-if="field.defaultValue" class="text-xs text-gray-500">Défaut: {{ field.defaultValue }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="subComponent.pieces && subComponent.pieces.length > 0">
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<span class="text-xs text-gray-600 font-medium">Pièces</span>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost btn-xs p-1"
|
||||
@click="toggleSubComponentPieceSection(subIndex)"
|
||||
>
|
||||
<svg
|
||||
class="w-3 h-3 transition-transform duration-200"
|
||||
:class="{ 'rotate-90': isSubComponentPieceSectionExpanded(subIndex) }"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="isSubComponentPieceSectionExpanded(subIndex)" class="space-y-1">
|
||||
<div
|
||||
v-for="(piece, pieceIndex) in subComponent.pieces"
|
||||
:key="pieceIndex"
|
||||
class="border border-gray-100 rounded p-2 bg-white"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<div class="flex items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost btn-xs p-1"
|
||||
@click="toggleSubPieceDetails(subIndex, pieceIndex)"
|
||||
>
|
||||
<svg
|
||||
class="w-3 h-3 transition-transform duration-200"
|
||||
:class="{ 'rotate-90': isSubPieceExpanded(subIndex, pieceIndex) }"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<svg class="w-3 h-3 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"></path>
|
||||
</svg>
|
||||
<span class="text-xs font-medium">{{ piece.name }}</span>
|
||||
<span v-if="!isSubPieceExpanded(subIndex, pieceIndex) && piece.reference" class="text-[10px] text-gray-500">(Ref: {{ piece.reference }})</span>
|
||||
</div>
|
||||
<div v-if="!isSubPieceExpanded(subIndex, pieceIndex) && quickPieceSummary(piece)" class="text-[10px] text-gray-500">
|
||||
{{ quickPieceSummary(piece) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isSubPieceExpanded(subIndex, pieceIndex)">
|
||||
<div class="grid grid-cols-2 gap-1 text-[11px] mb-1">
|
||||
<div v-if="piece.prestataire">
|
||||
<span class="font-medium">Prestataire:</span> {{ piece.prestataire }}
|
||||
</div>
|
||||
<div v-if="piece.emplacement">
|
||||
<span class="font-medium">Emplacement:</span> {{ piece.emplacement }}
|
||||
</div>
|
||||
<div v-if="piece.prix">
|
||||
<span class="font-medium">Prix:</span> {{ piece.prix }}€
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="piece.customFields && piece.customFields.length > 0" class="mt-1">
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<span class="text-[11px] font-medium text-gray-600">Champs personnalisés</span>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost btn-xs p-1"
|
||||
@click="toggleSubPieceCustomFields(subIndex, pieceIndex)"
|
||||
>
|
||||
<svg
|
||||
class="w-3 h-3 transition-transform duration-200"
|
||||
:class="{ 'rotate-90': isSubPieceCustomFieldSectionExpanded(subIndex, pieceIndex) }"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="isSubPieceCustomFieldSectionExpanded(subIndex, pieceIndex)" class="grid grid-cols-1 md:grid-cols-2 gap-1">
|
||||
<div v-for="field in piece.customFields" :key="field.id || field.name" class="bg-gray-50 rounded p-1">
|
||||
<div class="text-[11px] font-medium">{{ field.name }}</div>
|
||||
<div class="text-[10px] text-gray-600">{{ field.type }}</div>
|
||||
<div v-if="field.required" class="text-[10px] text-red-500">Obligatoire</div>
|
||||
<div v-if="field.defaultValue" class="text-[10px] text-gray-500">Défaut: {{ field.defaultValue }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
import { ref, reactive, computed, watch, onMounted } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
component: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
globalExpandState: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
const expanded = ref(true)
|
||||
const expandedSections = reactive({
|
||||
customFields: true,
|
||||
pieces: true,
|
||||
subComponents: true
|
||||
})
|
||||
|
||||
const expandedPieces = ref([])
|
||||
const expandedPieceCustomFields = ref([])
|
||||
const expandedSubComponents = ref([])
|
||||
const expandedSubComponentCustomFields = ref([])
|
||||
const expandedSubComponentPieces = ref([])
|
||||
const expandedSubComponentPieceCustomFields = reactive({})
|
||||
|
||||
const compactInfo = computed(() => {
|
||||
const infos = []
|
||||
if (props.component?.prestataire) {
|
||||
infos.push(props.component.prestataire)
|
||||
}
|
||||
if (props.component?.emplacement) {
|
||||
infos.push(props.component.emplacement)
|
||||
}
|
||||
return infos.join(' • ')
|
||||
})
|
||||
|
||||
const quickPieceSummary = (piece) => {
|
||||
const infos = []
|
||||
if (piece?.prestataire) infos.push(piece.prestataire)
|
||||
if (piece?.emplacement) infos.push(piece.emplacement)
|
||||
return infos.join(' • ')
|
||||
}
|
||||
|
||||
const quickSubSummary = (subComponent) => {
|
||||
const infos = []
|
||||
if (subComponent?.prestataire) infos.push(subComponent.prestataire)
|
||||
if (subComponent?.emplacement) infos.push(subComponent.emplacement)
|
||||
return infos.join(' • ')
|
||||
}
|
||||
|
||||
const applyGlobalExpansion = (expand) => {
|
||||
expanded.value = expand
|
||||
expandedSections.customFields = expand && (props.component?.customFields || []).length > 0
|
||||
expandedSections.pieces = expand && (props.component?.pieces || []).length > 0
|
||||
expandedSections.subComponents = expand && (props.component?.subComponents || []).length > 0
|
||||
|
||||
expandedPieces.value = (props.component?.pieces || []).map(() => expand)
|
||||
expandedPieceCustomFields.value = (props.component?.pieces || []).map(() => expand)
|
||||
|
||||
expandedSubComponents.value = (props.component?.subComponents || []).map(() => expand)
|
||||
expandedSubComponentCustomFields.value = (props.component?.subComponents || []).map(() => expand)
|
||||
expandedSubComponentPieces.value = (props.component?.subComponents || []).map(() => expand)
|
||||
|
||||
Object.keys(expandedSubComponentPieceCustomFields).forEach((key) => delete expandedSubComponentPieceCustomFields[key])
|
||||
;(props.component?.subComponents || []).forEach((subComponent, subIndex) => {
|
||||
expandedSubComponentPieceCustomFields[subIndex] = (subComponent.pieces || []).map(() => ({
|
||||
expanded: expand,
|
||||
customFields: expand
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
const initializeExpansionState = () => {
|
||||
const initialExpand = props.globalExpandState ? props.globalExpandState.expanded : true
|
||||
applyGlobalExpansion(initialExpand)
|
||||
}
|
||||
|
||||
watch(() => props.component, () => {
|
||||
initializeExpansionState()
|
||||
}, { deep: true })
|
||||
|
||||
onMounted(() => {
|
||||
initializeExpansionState()
|
||||
})
|
||||
|
||||
const toggleComponent = () => {
|
||||
expanded.value = !expanded.value
|
||||
}
|
||||
|
||||
const toggleSection = (section) => {
|
||||
expandedSections[section] = !expandedSections[section]
|
||||
}
|
||||
|
||||
const isPieceExpanded = (index) => expandedPieces.value[index]
|
||||
const togglePieceDetails = (index) => {
|
||||
expandedPieces.value[index] = !expandedPieces.value[index]
|
||||
}
|
||||
|
||||
const isPieceCustomFieldSectionExpanded = (index) => expandedPieceCustomFields.value[index]
|
||||
const togglePieceCustomFields = (index) => {
|
||||
expandedPieceCustomFields.value[index] = !expandedPieceCustomFields.value[index]
|
||||
}
|
||||
|
||||
const isSubComponentExpanded = (index) => expandedSubComponents.value[index]
|
||||
const toggleSubComponentDetails = (index) => {
|
||||
expandedSubComponents.value[index] = !expandedSubComponents.value[index]
|
||||
}
|
||||
|
||||
const isSubComponentCustomFieldSectionExpanded = (index) => expandedSubComponentCustomFields.value[index]
|
||||
const toggleSubComponentCustomFieldSection = (index) => {
|
||||
expandedSubComponentCustomFields.value[index] = !expandedSubComponentCustomFields.value[index]
|
||||
}
|
||||
|
||||
const isSubComponentPieceSectionExpanded = (index) => expandedSubComponentPieces.value[index]
|
||||
const toggleSubComponentPieceSection = (index) => {
|
||||
expandedSubComponentPieces.value[index] = !expandedSubComponentPieces.value[index]
|
||||
}
|
||||
|
||||
const isSubPieceExpanded = (subIndex, pieceIndex) => expandedSubComponentPieceCustomFields[subIndex]?.[pieceIndex]?.expanded ?? false
|
||||
|
||||
const ensureSubPieceEntry = (subIndex, pieceIndex) => {
|
||||
if (!expandedSubComponentPieceCustomFields[subIndex]) {
|
||||
expandedSubComponentPieceCustomFields[subIndex] = []
|
||||
}
|
||||
if (!expandedSubComponentPieceCustomFields[subIndex][pieceIndex]) {
|
||||
expandedSubComponentPieceCustomFields[subIndex][pieceIndex] = { expanded: false, customFields: false }
|
||||
}
|
||||
}
|
||||
|
||||
const toggleSubPieceDetails = (subIndex, pieceIndex) => {
|
||||
ensureSubPieceEntry(subIndex, pieceIndex)
|
||||
expandedSubComponentPieceCustomFields[subIndex][pieceIndex].expanded = !expandedSubComponentPieceCustomFields[subIndex][pieceIndex].expanded
|
||||
}
|
||||
|
||||
const isSubPieceCustomFieldSectionExpanded = (subIndex, pieceIndex) => expandedSubComponentPieceCustomFields[subIndex]?.[pieceIndex]?.customFields ?? false
|
||||
const toggleSubPieceCustomFields = (subIndex, pieceIndex) => {
|
||||
ensureSubPieceEntry(subIndex, pieceIndex)
|
||||
expandedSubComponentPieceCustomFields[subIndex][pieceIndex].customFields = !expandedSubComponentPieceCustomFields[subIndex][pieceIndex].customFields
|
||||
}
|
||||
|
||||
watch(() => props.globalExpandState?.id, () => {
|
||||
if (props.globalExpandState) {
|
||||
applyGlobalExpansion(props.globalExpandState.expanded)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,36 @@
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline btn-sm"
|
||||
@click="toggleAllSections"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4 mr-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
v-if="allExpanded"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M18 12H6"
|
||||
></path>
|
||||
<path
|
||||
v-else
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 6v12m6-6H6"
|
||||
></path>
|
||||
</svg>
|
||||
{{ allExpanded ? 'Tout plier' : 'Tout déplier' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Informations de base du type -->
|
||||
<div class="card bg-base-100 shadow-lg">
|
||||
<div class="card-body">
|
||||
@@ -101,10 +132,29 @@
|
||||
>
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost btn-xs p-1"
|
||||
@click="toggleCustomFieldDetails(fieldIndex)"
|
||||
title="Plier / déplier le champ"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4 transition-transform duration-200"
|
||||
:class="{ 'rotate-90': isCustomFieldExpanded(fieldIndex) }"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<svg class="w-4 h-4 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"></path>
|
||||
</svg>
|
||||
<h5 class="text-sm font-medium">Champ personnalisé {{ fieldIndex + 1 }}</h5>
|
||||
<span v-if="!isCustomFieldExpanded(fieldIndex)" class="text-xs text-gray-500 truncate max-w-[160px]">
|
||||
{{ field.name || 'Sans nom' }}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@@ -118,7 +168,7 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div v-if="isCustomFieldExpanded(fieldIndex)" class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Nom du champ</span>
|
||||
@@ -149,7 +199,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-3">
|
||||
<div v-if="isCustomFieldExpanded(fieldIndex)" class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
v-model="field.required"
|
||||
@@ -173,7 +223,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Options pour les champs de type SELECT -->
|
||||
<div v-if="field.type === 'select'" class="mt-3">
|
||||
<div v-if="isCustomFieldExpanded(fieldIndex) && field.type === 'select'" class="mt-3">
|
||||
<label class="label">
|
||||
<span class="label-text">Options de la liste</span>
|
||||
<span class="label-text-alt">Une option par ligne</span>
|
||||
@@ -233,10 +283,29 @@
|
||||
>
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost btn-xs p-1"
|
||||
@click="togglePieceDetails(pieceIndex)"
|
||||
title="Plier / déplier la pièce"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4 transition-transform duration-200"
|
||||
:class="{ 'rotate-90': isPieceExpanded(pieceIndex) }"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<svg class="w-4 h-4 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"></path>
|
||||
</svg>
|
||||
<h5 class="text-sm font-medium">Pièce {{ pieceIndex + 1 }}</h5>
|
||||
<span v-if="!isPieceExpanded(pieceIndex)" class="text-xs text-gray-500 truncate max-w-[160px]">
|
||||
{{ piece.name || 'Sans nom' }}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@@ -250,7 +319,7 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div v-if="isPieceExpanded(pieceIndex)" class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Nom de la pièce</span>
|
||||
@@ -303,7 +372,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Champs personnalisés de la pièce -->
|
||||
<div class="mt-4">
|
||||
<div v-if="isPieceExpanded(pieceIndex)" class="mt-4">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="text-sm font-medium">Champs personnalisés de cette pièce :</span>
|
||||
<button
|
||||
@@ -325,7 +394,28 @@
|
||||
class="border border-gray-200 rounded p-2 bg-white"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-xs font-medium">Champ {{ fieldIndex + 1 }}</span>
|
||||
<div class="flex items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost btn-xs p-1"
|
||||
@click="togglePieceCustomFieldDetails(pieceIndex, fieldIndex)"
|
||||
title="Plier / déplier le champ"
|
||||
>
|
||||
<svg
|
||||
class="w-3 h-3 transition-transform duration-200"
|
||||
:class="{ 'rotate-90': isPieceCustomFieldExpanded(pieceIndex, fieldIndex) }"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<span class="text-xs font-medium">Champ {{ fieldIndex + 1 }}</span>
|
||||
<span v-if="!isPieceCustomFieldExpanded(pieceIndex, fieldIndex)" class="text-[10px] text-gray-500 truncate max-w-[120px]">
|
||||
{{ field.name || 'Sans nom' }}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@click="removePieceCustomField(pieceIndex, fieldIndex)"
|
||||
@@ -337,7 +427,7 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||
<div v-if="isPieceCustomFieldExpanded(pieceIndex, fieldIndex)" class="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||
<input
|
||||
v-model="field.name"
|
||||
type="text"
|
||||
@@ -354,7 +444,7 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-2 mt-2">
|
||||
<div v-if="isPieceCustomFieldExpanded(pieceIndex, fieldIndex)" class="grid grid-cols-1 md:grid-cols-2 gap-2 mt-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
v-model="field.required"
|
||||
@@ -372,7 +462,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Options pour les champs de type SELECT -->
|
||||
<div v-if="field.type === 'select'" class="mt-2">
|
||||
<div v-if="isPieceCustomFieldExpanded(pieceIndex, fieldIndex) && field.type === 'select'" class="mt-2">
|
||||
<textarea
|
||||
v-model="field.optionsText"
|
||||
placeholder="Option 1 Option 2 Option 3"
|
||||
@@ -431,10 +521,29 @@
|
||||
>
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost btn-xs p-1"
|
||||
@click="toggleComponentDetails(componentIndex)"
|
||||
title="Plier / déplier le composant"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4 transition-transform duration-200"
|
||||
:class="{ 'rotate-90': isComponentExpanded(componentIndex) }"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<svg class="w-4 h-4 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z"></path>
|
||||
</svg>
|
||||
<h5 class="text-sm font-medium">Composant {{ componentIndex + 1 }}</h5>
|
||||
<span v-if="!isComponentExpanded(componentIndex)" class="text-xs text-gray-500 truncate max-w-[160px]">
|
||||
{{ component.name || 'Sans nom' }}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@@ -448,7 +557,7 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div v-if="isComponentExpanded(componentIndex)" class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Nom du composant</span>
|
||||
@@ -501,7 +610,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Champs personnalisés du composant -->
|
||||
<div class="mt-4">
|
||||
<div v-if="isComponentExpanded(componentIndex)" class="mt-4">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="text-sm font-medium">Champs personnalisés du composant :</span>
|
||||
<button
|
||||
@@ -523,7 +632,28 @@
|
||||
class="border border-gray-200 rounded p-2 bg-white"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-xs font-medium">Champ {{ fieldIndex + 1 }}</span>
|
||||
<div class="flex items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost btn-xs p-1"
|
||||
@click="toggleComponentCustomFieldDetails(componentIndex, fieldIndex)"
|
||||
title="Plier / déplier le champ"
|
||||
>
|
||||
<svg
|
||||
class="w-3 h-3 transition-transform duration-200"
|
||||
:class="{ 'rotate-90': isComponentCustomFieldExpanded(componentIndex, fieldIndex) }"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<span class="text-xs font-medium">Champ {{ fieldIndex + 1 }}</span>
|
||||
<span v-if="!isComponentCustomFieldExpanded(componentIndex, fieldIndex)" class="text-[10px] text-gray-500 truncate max-w-[120px]">
|
||||
{{ field.name || 'Sans nom' }}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@click="removeComponentCustomField(componentIndex, fieldIndex)"
|
||||
@@ -535,7 +665,7 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||
<div v-if="isComponentCustomFieldExpanded(componentIndex, fieldIndex)" class="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||
<input
|
||||
v-model="field.name"
|
||||
type="text"
|
||||
@@ -552,7 +682,7 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-2 mt-2">
|
||||
<div v-if="isComponentCustomFieldExpanded(componentIndex, fieldIndex)" class="grid grid-cols-1 md:grid-cols-2 gap-2 mt-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
v-model="field.required"
|
||||
@@ -570,7 +700,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Options pour les champs de type SELECT -->
|
||||
<div v-if="field.type === 'select'" class="mt-2">
|
||||
<div v-if="isComponentCustomFieldExpanded(componentIndex, fieldIndex) && field.type === 'select'" class="mt-2">
|
||||
<textarea
|
||||
v-model="field.optionsText"
|
||||
placeholder="Option 1 Option 2 Option 3"
|
||||
@@ -606,15 +736,40 @@
|
||||
>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost btn-xs p-1"
|
||||
@click="toggleComponentPieceDetails(componentIndex, pieceIndex)"
|
||||
title="Plier / déplier la pièce"
|
||||
>
|
||||
<svg
|
||||
class="w-3 h-3 transition-transform duration-200"
|
||||
:class="{ 'rotate-90': isComponentPieceExpanded(componentIndex, pieceIndex) }"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<svg class="w-3 h-3 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"></path>
|
||||
</svg>
|
||||
<input
|
||||
v-model="piece.name"
|
||||
type="text"
|
||||
:placeholder="`Pièce ${pieceIndex + 1}`"
|
||||
class="input input-bordered input-xs flex-1"
|
||||
/>
|
||||
<span class="text-xs font-medium">Pièce {{ pieceIndex + 1 }}</span>
|
||||
<template v-if="isComponentPieceExpanded(componentIndex, pieceIndex)">
|
||||
<input
|
||||
v-model="piece.name"
|
||||
type="text"
|
||||
:placeholder="`Nom de la pièce ${pieceIndex + 1}`"
|
||||
class="input input-bordered input-xs flex-1"
|
||||
/>
|
||||
</template>
|
||||
<span
|
||||
v-else
|
||||
class="text-[10px] text-gray-500 truncate max-w-[120px]"
|
||||
>
|
||||
{{ piece.name || 'Sans nom' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex gap-1">
|
||||
<button
|
||||
@@ -641,7 +796,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Champs personnalisés de cette pièce -->
|
||||
<div v-if="piece.customFields && piece.customFields.length > 0" class="mt-2 ml-4">
|
||||
<div v-if="isComponentPieceExpanded(componentIndex, pieceIndex) && piece.customFields && piece.customFields.length > 0" class="mt-2 ml-4">
|
||||
<div class="space-y-1">
|
||||
<div
|
||||
v-for="(field, fieldIndex) in piece.customFields"
|
||||
@@ -649,7 +804,28 @@
|
||||
class="border border-gray-100 rounded p-1 bg-gray-50"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<span class="text-xs">Champ {{ fieldIndex + 1 }}</span>
|
||||
<div class="flex items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost btn-xs p-1"
|
||||
@click="toggleComponentPieceCustomFieldDetails(componentIndex, pieceIndex, fieldIndex)"
|
||||
title="Plier / déplier le champ"
|
||||
>
|
||||
<svg
|
||||
class="w-3 h-3 transition-transform duration-200"
|
||||
:class="{ 'rotate-90': isComponentPieceCustomFieldExpanded(componentIndex, pieceIndex, fieldIndex) }"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<span class="text-xs">Champ {{ fieldIndex + 1 }}</span>
|
||||
<span v-if="!isComponentPieceCustomFieldExpanded(componentIndex, pieceIndex, fieldIndex)" class="text-[10px] text-gray-500 truncate max-w-[100px]">
|
||||
{{ field.name || 'Sans nom' }}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@click="removePieceCustomFieldFromComponent(componentIndex, pieceIndex, fieldIndex)"
|
||||
@@ -661,7 +837,7 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-1">
|
||||
<div v-if="isComponentPieceCustomFieldExpanded(componentIndex, pieceIndex, fieldIndex)" class="grid grid-cols-2 gap-1">
|
||||
<input
|
||||
v-model="field.name"
|
||||
type="text"
|
||||
@@ -678,7 +854,7 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-1 mt-1">
|
||||
<div v-if="isComponentPieceCustomFieldExpanded(componentIndex, pieceIndex, fieldIndex)" class="grid grid-cols-2 gap-1 mt-1">
|
||||
<div class="flex items-center gap-1">
|
||||
<input
|
||||
v-model="field.required"
|
||||
@@ -696,7 +872,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Options pour les champs de type SELECT -->
|
||||
<div v-if="field.type === 'select'" class="mt-1">
|
||||
<div v-if="isComponentPieceCustomFieldExpanded(componentIndex, pieceIndex, fieldIndex) && field.type === 'select'" class="mt-1">
|
||||
<textarea
|
||||
v-model="field.optionsText"
|
||||
placeholder="Option 1 Option 2 Option 3"
|
||||
@@ -738,7 +914,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import { ref, reactive, watch, onMounted } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
@@ -755,11 +931,21 @@ const emit = defineEmits(['update:modelValue', 'submit'])
|
||||
|
||||
// État des sections dépliées/repliées
|
||||
const expandedSections = reactive({
|
||||
customFields: true,
|
||||
machinePieces: true,
|
||||
components: true
|
||||
customFields: false,
|
||||
machinePieces: false,
|
||||
components: false
|
||||
})
|
||||
|
||||
// État de dépliage pour chaque élément de l'arborescence
|
||||
const allExpanded = ref(false)
|
||||
const expandedCustomFields = ref([])
|
||||
const expandedMachinePieces = ref([])
|
||||
const expandedPieceCustomFields = reactive({})
|
||||
const expandedComponents = ref([])
|
||||
const expandedComponentCustomFields = reactive({})
|
||||
const expandedComponentPieces = reactive({})
|
||||
const expandedComponentPieceCustomFields = reactive({})
|
||||
|
||||
// Initialiser les données du formulaire
|
||||
const formData = reactive({
|
||||
name: props.modelValue?.name || '',
|
||||
@@ -779,8 +965,189 @@ watch(formData, (newValue) => {
|
||||
// Méthode pour basculer l'état d'une section
|
||||
const toggleSection = (sectionName) => {
|
||||
expandedSections[sectionName] = !expandedSections[sectionName]
|
||||
if (!expandedSections[sectionName]) {
|
||||
allExpanded.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const isCustomFieldExpanded = (index) => {
|
||||
if (expandedCustomFields.value[index] === undefined) {
|
||||
expandedCustomFields.value[index] = allExpanded.value
|
||||
}
|
||||
return expandedCustomFields.value[index]
|
||||
}
|
||||
|
||||
const toggleCustomFieldDetails = (index) => {
|
||||
expandedCustomFields.value[index] = !isCustomFieldExpanded(index)
|
||||
if (!expandedCustomFields.value[index]) {
|
||||
allExpanded.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const ensurePieceFieldState = (pieceIndex) => {
|
||||
if (!expandedPieceCustomFields[pieceIndex]) {
|
||||
expandedPieceCustomFields[pieceIndex] = []
|
||||
}
|
||||
}
|
||||
|
||||
const isPieceExpanded = (index) => {
|
||||
if (expandedMachinePieces.value[index] === undefined) {
|
||||
expandedMachinePieces.value[index] = allExpanded.value
|
||||
}
|
||||
return expandedMachinePieces.value[index]
|
||||
}
|
||||
|
||||
const togglePieceDetails = (index) => {
|
||||
expandedMachinePieces.value[index] = !isPieceExpanded(index)
|
||||
if (!expandedMachinePieces.value[index]) {
|
||||
allExpanded.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const isPieceCustomFieldExpanded = (pieceIndex, fieldIndex) => {
|
||||
ensurePieceFieldState(pieceIndex)
|
||||
if (expandedPieceCustomFields[pieceIndex][fieldIndex] === undefined) {
|
||||
expandedPieceCustomFields[pieceIndex][fieldIndex] = allExpanded.value
|
||||
}
|
||||
return expandedPieceCustomFields[pieceIndex][fieldIndex]
|
||||
}
|
||||
|
||||
const togglePieceCustomFieldDetails = (pieceIndex, fieldIndex) => {
|
||||
ensurePieceFieldState(pieceIndex)
|
||||
expandedPieceCustomFields[pieceIndex][fieldIndex] = !isPieceCustomFieldExpanded(pieceIndex, fieldIndex)
|
||||
if (!expandedPieceCustomFields[pieceIndex][fieldIndex]) {
|
||||
allExpanded.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const ensureComponentState = (componentIndex) => {
|
||||
if (!expandedComponentCustomFields[componentIndex]) {
|
||||
expandedComponentCustomFields[componentIndex] = []
|
||||
}
|
||||
if (!expandedComponentPieces[componentIndex]) {
|
||||
expandedComponentPieces[componentIndex] = []
|
||||
}
|
||||
if (!expandedComponentPieceCustomFields[componentIndex]) {
|
||||
expandedComponentPieceCustomFields[componentIndex] = {}
|
||||
}
|
||||
}
|
||||
|
||||
const isComponentExpanded = (index) => {
|
||||
if (expandedComponents.value[index] === undefined) {
|
||||
expandedComponents.value[index] = allExpanded.value
|
||||
}
|
||||
return expandedComponents.value[index]
|
||||
}
|
||||
|
||||
const toggleComponentDetails = (index) => {
|
||||
expandedComponents.value[index] = !isComponentExpanded(index)
|
||||
if (!expandedComponents.value[index]) {
|
||||
allExpanded.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const isComponentCustomFieldExpanded = (componentIndex, fieldIndex) => {
|
||||
ensureComponentState(componentIndex)
|
||||
if (expandedComponentCustomFields[componentIndex][fieldIndex] === undefined) {
|
||||
expandedComponentCustomFields[componentIndex][fieldIndex] = allExpanded.value
|
||||
}
|
||||
return expandedComponentCustomFields[componentIndex][fieldIndex]
|
||||
}
|
||||
|
||||
const toggleComponentCustomFieldDetails = (componentIndex, fieldIndex) => {
|
||||
ensureComponentState(componentIndex)
|
||||
expandedComponentCustomFields[componentIndex][fieldIndex] = !isComponentCustomFieldExpanded(componentIndex, fieldIndex)
|
||||
if (!expandedComponentCustomFields[componentIndex][fieldIndex]) {
|
||||
allExpanded.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const ensureComponentPieceState = (componentIndex, pieceIndex) => {
|
||||
ensureComponentState(componentIndex)
|
||||
if (!expandedComponentPieceCustomFields[componentIndex][pieceIndex]) {
|
||||
expandedComponentPieceCustomFields[componentIndex][pieceIndex] = []
|
||||
}
|
||||
}
|
||||
|
||||
const isComponentPieceExpanded = (componentIndex, pieceIndex) => {
|
||||
ensureComponentState(componentIndex)
|
||||
if (expandedComponentPieces[componentIndex][pieceIndex] === undefined) {
|
||||
expandedComponentPieces[componentIndex][pieceIndex] = allExpanded.value
|
||||
}
|
||||
return expandedComponentPieces[componentIndex][pieceIndex]
|
||||
}
|
||||
|
||||
const toggleComponentPieceDetails = (componentIndex, pieceIndex) => {
|
||||
ensureComponentState(componentIndex)
|
||||
expandedComponentPieces[componentIndex][pieceIndex] = !isComponentPieceExpanded(componentIndex, pieceIndex)
|
||||
if (!expandedComponentPieces[componentIndex][pieceIndex]) {
|
||||
allExpanded.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const isComponentPieceCustomFieldExpanded = (componentIndex, pieceIndex, fieldIndex) => {
|
||||
ensureComponentPieceState(componentIndex, pieceIndex)
|
||||
if (expandedComponentPieceCustomFields[componentIndex][pieceIndex][fieldIndex] === undefined) {
|
||||
expandedComponentPieceCustomFields[componentIndex][pieceIndex][fieldIndex] = allExpanded.value
|
||||
}
|
||||
return expandedComponentPieceCustomFields[componentIndex][pieceIndex][fieldIndex]
|
||||
}
|
||||
|
||||
const toggleComponentPieceCustomFieldDetails = (componentIndex, pieceIndex, fieldIndex) => {
|
||||
ensureComponentPieceState(componentIndex, pieceIndex)
|
||||
expandedComponentPieceCustomFields[componentIndex][pieceIndex][fieldIndex] = !isComponentPieceCustomFieldExpanded(componentIndex, pieceIndex, fieldIndex)
|
||||
if (!expandedComponentPieceCustomFields[componentIndex][pieceIndex][fieldIndex]) {
|
||||
allExpanded.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const clearNestedExpansionState = () => {
|
||||
Object.keys(expandedPieceCustomFields).forEach((key) => delete expandedPieceCustomFields[key])
|
||||
Object.keys(expandedComponentCustomFields).forEach((key) => delete expandedComponentCustomFields[key])
|
||||
Object.keys(expandedComponentPieces).forEach((key) => delete expandedComponentPieces[key])
|
||||
Object.keys(expandedComponentPieceCustomFields).forEach((key) => delete expandedComponentPieceCustomFields[key])
|
||||
}
|
||||
|
||||
const setAllExpanded = (value) => {
|
||||
allExpanded.value = value
|
||||
|
||||
expandedSections.customFields = value
|
||||
expandedSections.machinePieces = value
|
||||
expandedSections.components = value
|
||||
|
||||
expandedCustomFields.value = formData.customFields.map(() => value)
|
||||
expandedMachinePieces.value = formData.machinePieces.map(() => value)
|
||||
expandedComponents.value = formData.components.map(() => value)
|
||||
|
||||
clearNestedExpansionState()
|
||||
|
||||
formData.machinePieces.forEach((piece, pieceIndex) => {
|
||||
expandedPieceCustomFields[pieceIndex] = (piece.customFields || []).map(() => value)
|
||||
})
|
||||
|
||||
formData.components.forEach((component, componentIndex) => {
|
||||
expandedComponentCustomFields[componentIndex] = (component.customFields || []).map(() => value)
|
||||
expandedComponentPieces[componentIndex] = (component.pieces || []).map(() => value)
|
||||
expandedComponentPieceCustomFields[componentIndex] = {}
|
||||
|
||||
component.pieces?.forEach((piece, pieceIndex) => {
|
||||
expandedComponentPieceCustomFields[componentIndex][pieceIndex] = (piece.customFields || []).map(() => value)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const toggleAllSections = () => {
|
||||
setAllExpanded(!allExpanded.value)
|
||||
}
|
||||
|
||||
const initializeExpansionState = () => {
|
||||
setAllExpanded(false)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initializeExpansionState()
|
||||
})
|
||||
|
||||
// Méthodes pour les champs personnalisés
|
||||
const addCustomField = () => {
|
||||
formData.customFields.push({
|
||||
@@ -790,10 +1157,12 @@ const addCustomField = () => {
|
||||
defaultValue: '',
|
||||
optionsText: ''
|
||||
})
|
||||
expandedCustomFields.value.push(true)
|
||||
}
|
||||
|
||||
const removeCustomField = (index) => {
|
||||
formData.customFields.splice(index, 1)
|
||||
expandedCustomFields.value.splice(index, 1)
|
||||
}
|
||||
|
||||
const updateFieldOptions = (fieldIndex) => {
|
||||
@@ -812,10 +1181,26 @@ const addMachinePiece = () => {
|
||||
prix: null,
|
||||
customFields: []
|
||||
})
|
||||
expandedMachinePieces.value.push(true)
|
||||
const newIndex = formData.machinePieces.length - 1
|
||||
expandedPieceCustomFields[newIndex] = []
|
||||
}
|
||||
|
||||
const removeMachinePiece = (index) => {
|
||||
formData.machinePieces.splice(index, 1)
|
||||
expandedMachinePieces.value.splice(index, 1)
|
||||
delete expandedPieceCustomFields[index]
|
||||
|
||||
// Re-index the remaining nested states
|
||||
const reorderedPieceStates = {}
|
||||
Object.keys(expandedPieceCustomFields)
|
||||
.map(key => Number(key))
|
||||
.sort((a, b) => a - b)
|
||||
.forEach((key, position) => {
|
||||
reorderedPieceStates[position] = expandedPieceCustomFields[key]
|
||||
})
|
||||
Object.keys(expandedPieceCustomFields).forEach(key => delete expandedPieceCustomFields[key])
|
||||
Object.assign(expandedPieceCustomFields, reorderedPieceStates)
|
||||
}
|
||||
|
||||
const addPieceCustomField = (pieceIndex) => {
|
||||
@@ -829,12 +1214,16 @@ const addPieceCustomField = (pieceIndex) => {
|
||||
defaultValue: '',
|
||||
optionsText: ''
|
||||
})
|
||||
ensurePieceFieldState(pieceIndex)
|
||||
expandedPieceCustomFields[pieceIndex].push(true)
|
||||
}
|
||||
|
||||
const removePieceCustomField = (pieceIndex, fieldIndex) => {
|
||||
if (formData.machinePieces[pieceIndex].customFields) {
|
||||
formData.machinePieces[pieceIndex].customFields.splice(fieldIndex, 1)
|
||||
}
|
||||
ensurePieceFieldState(pieceIndex)
|
||||
expandedPieceCustomFields[pieceIndex].splice(fieldIndex, 1)
|
||||
}
|
||||
|
||||
const updatePieceFieldOptions = (pieceIndex, fieldIndex) => {
|
||||
@@ -854,10 +1243,35 @@ const addComponent = () => {
|
||||
customFields: [],
|
||||
pieces: []
|
||||
})
|
||||
expandedComponents.value.push(true)
|
||||
const newIndex = formData.components.length - 1
|
||||
expandedComponentCustomFields[newIndex] = []
|
||||
expandedComponentPieces[newIndex] = []
|
||||
expandedComponentPieceCustomFields[newIndex] = {}
|
||||
}
|
||||
|
||||
const removeComponent = (index) => {
|
||||
formData.components.splice(index, 1)
|
||||
expandedComponents.value.splice(index, 1)
|
||||
delete expandedComponentCustomFields[index]
|
||||
delete expandedComponentPieces[index]
|
||||
delete expandedComponentPieceCustomFields[index]
|
||||
|
||||
const reorder = (source) => {
|
||||
const reordered = {}
|
||||
Object.keys(source)
|
||||
.map(key => Number(key))
|
||||
.sort((a, b) => a - b)
|
||||
.forEach((key, position) => {
|
||||
reordered[position] = source[key]
|
||||
})
|
||||
Object.keys(source).forEach(key => delete source[key])
|
||||
Object.assign(source, reordered)
|
||||
}
|
||||
|
||||
reorder(expandedComponentCustomFields)
|
||||
reorder(expandedComponentPieces)
|
||||
reorder(expandedComponentPieceCustomFields)
|
||||
}
|
||||
|
||||
const addComponentCustomField = (componentIndex) => {
|
||||
@@ -871,12 +1285,16 @@ const addComponentCustomField = (componentIndex) => {
|
||||
defaultValue: '',
|
||||
optionsText: ''
|
||||
})
|
||||
ensureComponentState(componentIndex)
|
||||
expandedComponentCustomFields[componentIndex].push(true)
|
||||
}
|
||||
|
||||
const removeComponentCustomField = (componentIndex, fieldIndex) => {
|
||||
if (formData.components[componentIndex].customFields) {
|
||||
formData.components[componentIndex].customFields.splice(fieldIndex, 1)
|
||||
}
|
||||
ensureComponentState(componentIndex)
|
||||
expandedComponentCustomFields[componentIndex].splice(fieldIndex, 1)
|
||||
}
|
||||
|
||||
const updateComponentFieldOptions = (componentIndex, fieldIndex) => {
|
||||
@@ -897,12 +1315,32 @@ const addComponentPiece = (componentIndex) => {
|
||||
prix: null,
|
||||
customFields: []
|
||||
})
|
||||
ensureComponentState(componentIndex)
|
||||
expandedComponentPieces[componentIndex].push(true)
|
||||
const newPieceIndex = formData.components[componentIndex].pieces.length - 1
|
||||
ensureComponentPieceState(componentIndex, newPieceIndex)
|
||||
}
|
||||
|
||||
const removeComponentPiece = (componentIndex, pieceIndex) => {
|
||||
if (formData.components[componentIndex].pieces) {
|
||||
formData.components[componentIndex].pieces.splice(pieceIndex, 1)
|
||||
}
|
||||
ensureComponentState(componentIndex)
|
||||
expandedComponentPieces[componentIndex].splice(pieceIndex, 1)
|
||||
if (expandedComponentPieceCustomFields[componentIndex]) {
|
||||
delete expandedComponentPieceCustomFields[componentIndex][pieceIndex]
|
||||
|
||||
const reorderedPieceStates = {}
|
||||
Object.keys(expandedComponentPieceCustomFields[componentIndex])
|
||||
.map(key => Number(key))
|
||||
.sort((a, b) => a - b)
|
||||
.forEach((key, position) => {
|
||||
reorderedPieceStates[position] = expandedComponentPieceCustomFields[componentIndex][key]
|
||||
})
|
||||
|
||||
Object.keys(expandedComponentPieceCustomFields[componentIndex]).forEach(key => delete expandedComponentPieceCustomFields[componentIndex][key])
|
||||
Object.assign(expandedComponentPieceCustomFields[componentIndex], reorderedPieceStates)
|
||||
}
|
||||
}
|
||||
|
||||
const addPieceCustomFieldToComponent = (componentIndex, pieceIndex) => {
|
||||
@@ -916,12 +1354,16 @@ const addPieceCustomFieldToComponent = (componentIndex, pieceIndex) => {
|
||||
defaultValue: '',
|
||||
optionsText: ''
|
||||
})
|
||||
ensureComponentPieceState(componentIndex, pieceIndex)
|
||||
expandedComponentPieceCustomFields[componentIndex][pieceIndex].push(true)
|
||||
}
|
||||
|
||||
const removePieceCustomFieldFromComponent = (componentIndex, pieceIndex, fieldIndex) => {
|
||||
if (formData.components[componentIndex]?.pieces?.[pieceIndex]?.customFields) {
|
||||
formData.components[componentIndex].pieces[pieceIndex].customFields.splice(fieldIndex, 1)
|
||||
}
|
||||
ensureComponentPieceState(componentIndex, pieceIndex)
|
||||
expandedComponentPieceCustomFields[componentIndex][pieceIndex].splice(fieldIndex, 1)
|
||||
}
|
||||
|
||||
const updateComponentPieceFieldOptions = (componentIndex, pieceIndex, fieldIndex) => {
|
||||
@@ -941,5 +1383,6 @@ const resetForm = () => {
|
||||
machinePieces: props.modelValue?.machinePieces || [],
|
||||
components: props.modelValue?.components || []
|
||||
})
|
||||
initializeExpansionState()
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -2,38 +2,58 @@
|
||||
<div class="border border-gray-300 rounded-lg p-4 bg-base-100">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost btn-xs p-1"
|
||||
@click="toggleDetails"
|
||||
title="Plier / déplier la pièce"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4 transition-transform duration-200"
|
||||
:class="{ 'rotate-90': expanded }"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<svg class="w-5 h-5 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"></path>
|
||||
</svg>
|
||||
<h4 class="font-medium">{{ piece.name }}</h4>
|
||||
<span v-if="!expanded && piece.reference" class="text-xs text-gray-500">(Ref: {{ piece.reference }})</span>
|
||||
</div>
|
||||
<div v-if="!expanded && quickSummary" class="text-xs text-gray-500">
|
||||
{{ quickSummary }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Informations de la pièce -->
|
||||
<div class="space-y-1 mb-3">
|
||||
<div v-if="piece.reference" class="text-sm">
|
||||
<span class="font-medium">Référence:</span> {{ piece.reference }}
|
||||
<div v-if="expanded">
|
||||
<div class="space-y-1 mb-3">
|
||||
<div v-if="piece.reference" class="text-sm">
|
||||
<span class="font-medium">Référence:</span> {{ piece.reference }}
|
||||
</div>
|
||||
<div v-if="piece.prestataire" class="text-sm">
|
||||
<span class="font-medium">Prestataire:</span> {{ piece.prestataire }}
|
||||
</div>
|
||||
<div v-if="piece.emplacement" class="text-sm">
|
||||
<span class="font-medium">Emplacement:</span> {{ piece.emplacement }}
|
||||
</div>
|
||||
<div v-if="piece.prix" class="text-sm">
|
||||
<span class="font-medium">Prix:</span> {{ piece.prix }}€
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="piece.prestataire" class="text-sm">
|
||||
<span class="font-medium">Prestataire:</span> {{ piece.prestataire }}
|
||||
</div>
|
||||
<div v-if="piece.emplacement" class="text-sm">
|
||||
<span class="font-medium">Emplacement:</span> {{ piece.emplacement }}
|
||||
</div>
|
||||
<div v-if="piece.prix" class="text-sm">
|
||||
<span class="font-medium">Prix:</span> {{ piece.prix }}€
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Champs personnalisés de la pièce -->
|
||||
<div v-if="piece.customFields && piece.customFields.length > 0">
|
||||
<h5 class="text-sm font-medium text-gray-700 mb-2">Champs personnalisés:</h5>
|
||||
<div class="grid grid-cols-1 gap-2">
|
||||
<div v-for="field in piece.customFields" :key="field.id" class="bg-gray-50 rounded p-2">
|
||||
<div class="text-sm font-medium">{{ field.name }}</div>
|
||||
<div class="text-xs text-gray-600">{{ field.type }}</div>
|
||||
<div v-if="field.required" class="text-xs text-red-500">Obligatoire</div>
|
||||
<div v-if="field.defaultValue" class="text-xs text-gray-500">Défaut: {{ field.defaultValue }}</div>
|
||||
<div v-if="piece.customFields && piece.customFields.length > 0">
|
||||
<h5 class="text-sm font-medium text-gray-700 mb-2">Champs personnalisés:</h5>
|
||||
<div class="grid grid-cols-1 gap-2">
|
||||
<div v-for="field in piece.customFields" :key="field.id || field.name" class="bg-gray-50 rounded p-2">
|
||||
<div class="text-sm font-medium">{{ field.name }}</div>
|
||||
<div class="text-xs text-gray-600">{{ field.type }}</div>
|
||||
<div v-if="field.required" class="text-xs text-red-500">Obligatoire</div>
|
||||
<div v-if="field.defaultValue" class="text-xs text-gray-500">Défaut: {{ field.defaultValue }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -41,10 +61,43 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
import { ref, computed, watch } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
piece: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
globalExpandState: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
const expanded = ref(true)
|
||||
|
||||
const quickSummary = computed(() => {
|
||||
const infos = []
|
||||
if (props.piece?.prestataire) {
|
||||
infos.push(props.piece.prestataire)
|
||||
}
|
||||
if (props.piece?.emplacement) {
|
||||
infos.push(props.piece.emplacement)
|
||||
}
|
||||
return infos.join(' • ')
|
||||
})
|
||||
|
||||
watch(() => props.piece, () => {
|
||||
expanded.value = props.globalExpandState ? props.globalExpandState.expanded : true
|
||||
}, { deep: true })
|
||||
|
||||
watch(() => props.globalExpandState?.id, () => {
|
||||
if (props.globalExpandState) {
|
||||
expanded.value = props.globalExpandState.expanded
|
||||
}
|
||||
})
|
||||
|
||||
const toggleDetails = () => {
|
||||
expanded.value = !expanded.value
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,8 +1,55 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline btn-sm"
|
||||
@click="toggleAllPieces"
|
||||
>
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
v-if="allExpanded"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M18 12H6"
|
||||
></path>
|
||||
<path
|
||||
v-else
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 6v12m6-6H6"
|
||||
></path>
|
||||
</svg>
|
||||
{{ allExpanded ? 'Tout plier' : 'Tout déplier' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-for="(piece, index) in pieces" :key="index" class="border border-gray-200 rounded-lg p-4 bg-gray-50">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h5 class="text-sm font-medium">Nouvelle pièce {{ index + 1 }}</h5>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost btn-xs p-1"
|
||||
@click="togglePieceDetails(index)"
|
||||
title="Plier / déplier la pièce"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4 transition-transform duration-200"
|
||||
:class="{ 'rotate-90': isPieceExpanded(index) }"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<h5 class="text-sm font-medium">Nouvelle pièce {{ index + 1 }}</h5>
|
||||
<span v-if="!isPieceExpanded(index)" class="text-xs text-gray-500 truncate max-w-[160px]">
|
||||
{{ piece.name || 'Sans nom' }}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@click="removePiece(index)"
|
||||
@@ -14,7 +61,7 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div v-if="isPieceExpanded(index)" class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Nom de la pièce</span>
|
||||
@@ -63,7 +110,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Champs personnalisés de cette pièce -->
|
||||
<div v-if="piece.customFields && piece.customFields.length > 0" class="mt-4">
|
||||
<div v-if="isPieceExpanded(index) && piece.customFields && piece.customFields.length > 0" class="mt-4">
|
||||
<h6 class="text-xs font-medium text-gray-600 mb-2">Champs personnalisés de cette pièce :</h6>
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
@@ -72,7 +119,28 @@
|
||||
class="border border-gray-200 rounded p-2 bg-white"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-xs font-medium">Champ {{ fieldIndex + 1 }}</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost btn-xs p-1"
|
||||
@click="togglePieceCustomFieldDetails(index, fieldIndex)"
|
||||
title="Plier / déplier le champ"
|
||||
>
|
||||
<svg
|
||||
class="w-3 h-3 transition-transform duration-200"
|
||||
:class="{ 'rotate-90': isPieceCustomFieldExpanded(index, fieldIndex) }"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<span class="text-xs font-medium">Champ {{ fieldIndex + 1 }}</span>
|
||||
<span v-if="!isPieceCustomFieldExpanded(index, fieldIndex)" class="text-[10px] text-gray-500 truncate max-w-[120px]">
|
||||
{{ field.name || 'Sans nom' }}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@click="removeCustomField(index, fieldIndex)"
|
||||
@@ -84,7 +152,7 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||
<div v-if="isPieceCustomFieldExpanded(index, fieldIndex)" class="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||
<div>
|
||||
<input
|
||||
v-model="field.name"
|
||||
@@ -105,7 +173,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-2 mt-2">
|
||||
<div v-if="isPieceCustomFieldExpanded(index, fieldIndex)" class="grid grid-cols-1 md:grid-cols-2 gap-2 mt-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
v-model="field.required"
|
||||
@@ -125,7 +193,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Options pour les champs de type SELECT -->
|
||||
<div v-if="field.type === 'select'" class="mt-2">
|
||||
<div v-if="isPieceCustomFieldExpanded(index, fieldIndex) && field.type === 'select'" class="mt-2">
|
||||
<textarea
|
||||
v-model="field.optionsText"
|
||||
placeholder="Option 1 Option 2 Option 3"
|
||||
@@ -138,7 +206,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Bouton pour ajouter des champs personnalisés -->
|
||||
<div class="mt-3">
|
||||
<div v-if="isPieceExpanded(index)" class="mt-3">
|
||||
<button
|
||||
type="button"
|
||||
@click="addCustomField(index)"
|
||||
@@ -166,7 +234,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ref, reactive, onMounted, watch } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
@@ -179,6 +247,80 @@ const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const pieces = ref(props.modelValue)
|
||||
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
pieces.value = newValue
|
||||
initializeExpansionState()
|
||||
})
|
||||
|
||||
const allExpanded = ref(false)
|
||||
const expandedPieces = ref([])
|
||||
const expandedPieceCustomFields = reactive({})
|
||||
|
||||
const ensurePieceFieldState = (pieceIndex) => {
|
||||
if (!expandedPieceCustomFields[pieceIndex]) {
|
||||
expandedPieceCustomFields[pieceIndex] = []
|
||||
}
|
||||
}
|
||||
|
||||
const isPieceExpanded = (index) => {
|
||||
if (expandedPieces.value[index] === undefined) {
|
||||
expandedPieces.value[index] = allExpanded.value
|
||||
}
|
||||
return expandedPieces.value[index]
|
||||
}
|
||||
|
||||
const togglePieceDetails = (index) => {
|
||||
expandedPieces.value[index] = !isPieceExpanded(index)
|
||||
if (!expandedPieces.value[index]) {
|
||||
allExpanded.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const isPieceCustomFieldExpanded = (pieceIndex, fieldIndex) => {
|
||||
ensurePieceFieldState(pieceIndex)
|
||||
if (expandedPieceCustomFields[pieceIndex][fieldIndex] === undefined) {
|
||||
expandedPieceCustomFields[pieceIndex][fieldIndex] = allExpanded.value
|
||||
}
|
||||
return expandedPieceCustomFields[pieceIndex][fieldIndex]
|
||||
}
|
||||
|
||||
const togglePieceCustomFieldDetails = (pieceIndex, fieldIndex) => {
|
||||
ensurePieceFieldState(pieceIndex)
|
||||
expandedPieceCustomFields[pieceIndex][fieldIndex] = !isPieceCustomFieldExpanded(pieceIndex, fieldIndex)
|
||||
if (!expandedPieceCustomFields[pieceIndex][fieldIndex]) {
|
||||
allExpanded.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const clearExpansionState = () => {
|
||||
expandedPieces.value = []
|
||||
Object.keys(expandedPieceCustomFields).forEach((key) => delete expandedPieceCustomFields[key])
|
||||
}
|
||||
|
||||
const setAllExpanded = (value) => {
|
||||
allExpanded.value = value
|
||||
expandedPieces.value = pieces.value.map(() => value)
|
||||
|
||||
Object.keys(expandedPieceCustomFields).forEach((key) => delete expandedPieceCustomFields[key])
|
||||
|
||||
pieces.value.forEach((piece, index) => {
|
||||
expandedPieceCustomFields[index] = (piece.customFields || []).map(() => value)
|
||||
})
|
||||
}
|
||||
|
||||
const toggleAllPieces = () => {
|
||||
setAllExpanded(!allExpanded.value)
|
||||
}
|
||||
|
||||
const initializeExpansionState = () => {
|
||||
clearExpansionState()
|
||||
setAllExpanded(false)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initializeExpansionState()
|
||||
})
|
||||
|
||||
// Méthodes pour les pièces
|
||||
const addPiece = () => {
|
||||
pieces.value.push({
|
||||
@@ -190,11 +332,26 @@ const addPiece = () => {
|
||||
customFields: []
|
||||
})
|
||||
emit('update:modelValue', pieces.value)
|
||||
expandedPieces.value.push(allExpanded.value)
|
||||
ensurePieceFieldState(pieces.value.length - 1)
|
||||
}
|
||||
|
||||
const removePiece = (index) => {
|
||||
pieces.value.splice(index, 1)
|
||||
emit('update:modelValue', pieces.value)
|
||||
expandedPieces.value.splice(index, 1)
|
||||
delete expandedPieceCustomFields[index]
|
||||
|
||||
const reordered = {}
|
||||
Object.keys(expandedPieceCustomFields)
|
||||
.map(key => Number(key))
|
||||
.sort((a, b) => a - b)
|
||||
.forEach((key, position) => {
|
||||
reordered[position] = expandedPieceCustomFields[key]
|
||||
})
|
||||
|
||||
Object.keys(expandedPieceCustomFields).forEach(key => delete expandedPieceCustomFields[key])
|
||||
Object.assign(expandedPieceCustomFields, reordered)
|
||||
}
|
||||
|
||||
const addCustomField = (pieceIndex) => {
|
||||
@@ -208,12 +365,16 @@ const addCustomField = (pieceIndex) => {
|
||||
defaultValue: '',
|
||||
optionsText: ''
|
||||
})
|
||||
ensurePieceFieldState(pieceIndex)
|
||||
expandedPieceCustomFields[pieceIndex].push(allExpanded.value)
|
||||
}
|
||||
|
||||
const removeCustomField = (pieceIndex, fieldIndex) => {
|
||||
if (pieces.value[pieceIndex].customFields) {
|
||||
pieces.value[pieceIndex].customFields.splice(fieldIndex, 1)
|
||||
}
|
||||
ensurePieceFieldState(pieceIndex)
|
||||
expandedPieceCustomFields[pieceIndex].splice(fieldIndex, 1)
|
||||
}
|
||||
|
||||
const updateFieldOptions = (pieceIndex, fieldIndex) => {
|
||||
@@ -221,4 +382,4 @@ const updateFieldOptions = (pieceIndex, fieldIndex) => {
|
||||
pieces.value[pieceIndex].customFields[fieldIndex].optionsText = pieces.value[pieceIndex].customFields[fieldIndex].optionsText.replace(/\r\n/g, '\n').replace(/\r/g, '\n')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -24,6 +24,36 @@
|
||||
</h2>
|
||||
|
||||
<form @submit.prevent="generateType" class="space-y-6">
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline btn-sm"
|
||||
@click="toggleAllSections"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4 mr-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
v-if="allSectionsExpanded"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M18 12H6"
|
||||
></path>
|
||||
<path
|
||||
v-else
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 6v12m6-6H6"
|
||||
></path>
|
||||
</svg>
|
||||
{{ allSectionsExpanded ? 'Tout plier' : 'Tout déplier' }}
|
||||
</button>
|
||||
</div>
|
||||
<!-- Basic Information -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="form-control">
|
||||
@@ -81,11 +111,31 @@
|
||||
|
||||
<!-- Machine Pieces (Root Level) -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Pièces de la machine</span>
|
||||
<span class="label-text-alt">Pièces directement attachées à la machine (optionnel)</span>
|
||||
</label>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost btn-sm p-1"
|
||||
@click="toggleSection('machinePieces')"
|
||||
title="Plier / déplier les pièces"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4 transition-transform duration-200"
|
||||
:class="{ 'rotate-90': sections.machinePieces }"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<label class="label m-0 flex flex-col items-start">
|
||||
<span class="label-text">Pièces de la machine</span>
|
||||
<span class="label-text-alt">Pièces directement attachées à la machine (optionnel)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="sections.machinePieces" class="space-y-4">
|
||||
<div v-for="(piece, index) in newType.machinePieces" :key="index" class="border border-gray-200 rounded-lg p-4 bg-gray-50">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h5 class="text-sm font-medium">Pièce {{ index + 1 }}</h5>
|
||||
@@ -221,12 +271,32 @@
|
||||
|
||||
<!-- Hierarchical Components -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Structure hiérarchique</span>
|
||||
<span class="label-text-alt">Définissez les composants, sous-composants et leurs pièces (optionnel)</span>
|
||||
</label>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost btn-sm p-1"
|
||||
@click="toggleSection('components')"
|
||||
title="Plier / déplier la structure"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4 transition-transform duration-200"
|
||||
:class="{ 'rotate-90': sections.components }"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<label class="label m-0 flex flex-col items-start">
|
||||
<span class="label-text">Structure hiérarchique</span>
|
||||
<span class="label-text-alt">Définissez les composants, sous-composants et leurs pièces (optionnel)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div v-if="sections.components" class="space-y-4">
|
||||
<!-- Root Components -->
|
||||
<div class="space-y-3">
|
||||
<h4 class="text-sm font-semibold text-gray-700">Composants principaux :</h4>
|
||||
@@ -236,6 +306,22 @@
|
||||
class="border border-gray-200 rounded-lg p-4 bg-gray-50"
|
||||
>
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost btn-xs p-1"
|
||||
@click="toggleComponentDetails(index)"
|
||||
title="Plier / déplier le composant"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4 transition-transform duration-200"
|
||||
:class="{ 'rotate-90': isComponentExpanded(index) }"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<svg class="w-4 h-4 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z"></path>
|
||||
</svg>
|
||||
@@ -257,6 +343,8 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="isComponentExpanded(index)">
|
||||
|
||||
<!-- Champs personnalisés du composant -->
|
||||
<div class="mb-3">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
@@ -882,7 +970,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
@click="addComponent"
|
||||
@@ -899,11 +988,31 @@
|
||||
|
||||
<!-- Custom Fields -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Champs personnalisés</span>
|
||||
<span class="label-text-alt">Définissez des champs personnalisés pour ce type de machine (optionnel)</span>
|
||||
</label>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost btn-sm p-1"
|
||||
@click="toggleSection('customFields')"
|
||||
title="Plier / déplier les champs personnalisés"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4 transition-transform duration-200"
|
||||
:class="{ 'rotate-90': sections.customFields }"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<label class="label m-0 flex flex-col items-start">
|
||||
<span class="label-text">Champs personnalisés</span>
|
||||
<span class="label-text-alt">Définissez des champs personnalisés pour ce type de machine (optionnel)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="sections.customFields" class="space-y-4">
|
||||
<div v-for="(field, index) in newType.customFields" :key="index" class="border border-gray-200 rounded-lg p-4 bg-gray-50">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h5 class="text-sm font-medium">Champ personnalisé {{ index + 1 }}</h5>
|
||||
@@ -1043,7 +1152,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useMachineTypesApi } from '~/composables/useMachineTypesApi'
|
||||
import { useToast } from '~/composables/useToast'
|
||||
|
||||
@@ -1071,6 +1180,33 @@ const newType = reactive({
|
||||
customFields: [] // Changed to empty array to make it optional
|
||||
})
|
||||
|
||||
const sections = reactive({
|
||||
machinePieces: true,
|
||||
components: true,
|
||||
customFields: true
|
||||
})
|
||||
const expandedComponents = reactive([])
|
||||
|
||||
const allSectionsExpanded = computed(() => sections.machinePieces && sections.components && sections.customFields)
|
||||
|
||||
const toggleSection = (section) => {
|
||||
sections[section] = !sections[section]
|
||||
}
|
||||
|
||||
const toggleAllSections = () => {
|
||||
const next = !allSectionsExpanded.value
|
||||
sections.machinePieces = next
|
||||
sections.components = next
|
||||
sections.customFields = next
|
||||
expandedComponents.splice(0, expandedComponents.length, ...newType.components.map(() => next))
|
||||
}
|
||||
|
||||
const isComponentExpanded = (index) => expandedComponents[index] ?? true
|
||||
|
||||
const toggleComponentDetails = (index) => {
|
||||
expandedComponents[index] = !isComponentExpanded(index)
|
||||
}
|
||||
|
||||
// Methods for hierarchical components
|
||||
const addComponent = () => {
|
||||
newType.components.push({
|
||||
@@ -1086,10 +1222,12 @@ const addComponent = () => {
|
||||
pieces: [{ name: '', customFields: [] }],
|
||||
customFields: []
|
||||
})
|
||||
expandedComponents.push(true)
|
||||
}
|
||||
|
||||
const removeComponent = (index) => {
|
||||
newType.components.splice(index, 1)
|
||||
expandedComponents.splice(index, 1)
|
||||
}
|
||||
|
||||
const addSubComponent = (component) => {
|
||||
@@ -1339,6 +1477,7 @@ const resetForm = () => {
|
||||
newType.components = [] // Reset to empty array
|
||||
newType.machinePieces = [] // Reset to empty array
|
||||
newType.customFields = [] // Reset to empty array
|
||||
expandedComponents.splice(0, expandedComponents.length)
|
||||
}
|
||||
|
||||
const generateType = async () => {
|
||||
@@ -1413,4 +1552,4 @@ onMounted(async () => {
|
||||
await loadMachineTypes()
|
||||
recentTypes.value = machineTypes.value.slice(-3).reverse()
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<p class="mt-4 text-gray-600">Chargement du type...</p>
|
||||
</div>
|
||||
|
||||
<!-- Edit Form -->
|
||||
<!-- Type Details -->
|
||||
<div v-else-if="type" class="my-8">
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
@@ -42,6 +42,37 @@
|
||||
<!-- Current Type Info -->
|
||||
<TypeInfoDisplay :type="type" />
|
||||
|
||||
<div v-if="hasExpandableContent" class="flex justify-end mb-6">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline btn-sm"
|
||||
@click="toggleGlobalExpand"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4 mr-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
v-if="globalExpandState.expanded"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M18 12H6"
|
||||
></path>
|
||||
<path
|
||||
v-else
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 6v12m6-6H6"
|
||||
></path>
|
||||
</svg>
|
||||
{{ globalExpandState.expanded ? 'Tout plier' : 'Tout déplier' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Affichage des composants existants -->
|
||||
<div v-if="type.components && type.components.length > 0" class="mb-8">
|
||||
<h3 class="text-lg font-semibold mb-4">Composants existants</h3>
|
||||
@@ -50,6 +81,7 @@
|
||||
v-for="(component, componentIndex) in type.components"
|
||||
:key="componentIndex"
|
||||
:component="component"
|
||||
:global-expand-state="globalExpandState"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -62,45 +94,10 @@
|
||||
v-for="(piece, pieceIndex) in type.machinePieces"
|
||||
:key="pieceIndex"
|
||||
:piece="piece"
|
||||
:global-expand-state="globalExpandState"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="saveChanges" class="space-y-6">
|
||||
<!-- Add Machine Pieces -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Ajouter des pièces de machine</span>
|
||||
<span class="label-text-alt">Nouvelles pièces directement attachées à la machine</span>
|
||||
</label>
|
||||
<TypeMachinePieceForm v-model="newMachinePieces" />
|
||||
</div>
|
||||
|
||||
<!-- Add Components -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Ajouter des composants</span>
|
||||
<span class="label-text-alt">Nouveaux composants principaux avec sous-composants</span>
|
||||
</label>
|
||||
<TypeComponentForm v-model="newComponents" />
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="card-actions justify-end">
|
||||
<button type="button" @click="resetForm" class="btn btn-outline">
|
||||
Réinitialiser
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary" :disabled="saving">
|
||||
<svg v-if="saving" class="w-5 h-5 mr-2 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
||||
</svg>
|
||||
<svg v-else class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||||
</svg>
|
||||
{{ saving ? 'Sauvegarde...' : 'Sauvegarder les modifications' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -121,127 +118,46 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useMachineTypesApi } from '~/composables/useMachineTypesApi'
|
||||
import { useToast } from '~/composables/useToast'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { getMachineTypeById, updateMachineType } = useMachineTypesApi()
|
||||
const { showSuccess, showError } = useToast()
|
||||
const { getMachineTypeById } = useMachineTypesApi()
|
||||
const { showError } = useToast()
|
||||
|
||||
const type = ref(null)
|
||||
const loading = ref(true)
|
||||
const saving = ref(false)
|
||||
|
||||
// Nouvelles données à ajouter
|
||||
const newMachinePieces = ref([])
|
||||
const newComponents = ref([])
|
||||
const globalExpandState = reactive({
|
||||
expanded: true,
|
||||
id: 0
|
||||
})
|
||||
|
||||
const resetForm = () => {
|
||||
newMachinePieces.value = []
|
||||
newComponents.value = []
|
||||
const hasExpandableContent = computed(() => {
|
||||
const componentCount = type.value?.components?.length || 0
|
||||
const pieceCount = type.value?.machinePieces?.length || 0
|
||||
return componentCount + pieceCount > 0
|
||||
})
|
||||
|
||||
const toggleGlobalExpand = () => {
|
||||
globalExpandState.expanded = !globalExpandState.expanded
|
||||
globalExpandState.id += 1
|
||||
}
|
||||
|
||||
const saveChanges = async () => {
|
||||
try {
|
||||
saving.value = true
|
||||
|
||||
// Préparer les données mises à jour
|
||||
const updatedType = {
|
||||
...type.value,
|
||||
machinePieces: [
|
||||
...(type.value.machinePieces || []),
|
||||
...newMachinePieces.value
|
||||
.filter(piece => piece.name.trim() !== '')
|
||||
.map(piece => ({
|
||||
name: piece.name,
|
||||
reference: piece.reference,
|
||||
prestataire: piece.prestataire,
|
||||
emplacement: piece.emplacement,
|
||||
prix: piece.prix,
|
||||
customFields: piece.customFields || []
|
||||
}))
|
||||
],
|
||||
components: [
|
||||
...(type.value.components || []),
|
||||
...newComponents.value
|
||||
.filter(comp => comp.name.trim() !== '')
|
||||
.map(comp => ({
|
||||
name: comp.name,
|
||||
reference: comp.reference,
|
||||
prestataire: comp.prestataire,
|
||||
emplacement: comp.emplacement,
|
||||
prix: comp.prix,
|
||||
customFields: comp.customFields || [],
|
||||
pieces: comp.pieces
|
||||
.filter(piece => piece.name.trim() !== '')
|
||||
.map(piece => ({
|
||||
name: piece.name,
|
||||
reference: piece.reference,
|
||||
prestataire: piece.prestataire,
|
||||
emplacement: piece.emplacement,
|
||||
prix: piece.prix,
|
||||
customFields: piece.customFields || []
|
||||
})),
|
||||
subComponents: comp.subComponents
|
||||
.filter(subComp => subComp.name.trim() !== '')
|
||||
.map(subComp => ({
|
||||
name: subComp.name,
|
||||
reference: subComp.reference,
|
||||
prestataire: subComp.prestataire,
|
||||
emplacement: subComp.emplacement,
|
||||
prix: subComp.prix,
|
||||
customFields: subComp.customFields || [],
|
||||
pieces: subComp.pieces
|
||||
.filter(piece => piece.name.trim() !== '')
|
||||
.map(piece => ({
|
||||
name: piece.name,
|
||||
reference: piece.reference,
|
||||
prestataire: piece.prestataire,
|
||||
emplacement: piece.emplacement,
|
||||
prix: piece.prix,
|
||||
customFields: piece.customFields || []
|
||||
}))
|
||||
}))
|
||||
}))
|
||||
]
|
||||
}
|
||||
|
||||
const result = await updateMachineType(type.value.id, updatedType)
|
||||
|
||||
if (result.success) {
|
||||
showSuccess('Type mis à jour avec succès !')
|
||||
router.push('/types')
|
||||
} else {
|
||||
showError('Erreur lors de la mise à jour du type')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la sauvegarde:', error)
|
||||
showError('Erreur lors de la sauvegarde')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Charger le type au montage
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const typeId = route.params.id
|
||||
console.log('=== EDIT PAGE LOADING ===')
|
||||
console.log('=== TYPE DETAIL PAGE LOADING ===')
|
||||
console.log('Loading type with ID:', typeId)
|
||||
console.log('Route params:', route.params)
|
||||
console.log('Current route:', route.path)
|
||||
|
||||
|
||||
const result = await getMachineTypeById(typeId)
|
||||
console.log('API Result:', result)
|
||||
|
||||
|
||||
if (result.success) {
|
||||
type.value = result.data
|
||||
console.log('Type loaded successfully:', type.value)
|
||||
console.log('Type ID:', type.value.id)
|
||||
console.log('Type name:', type.value.name)
|
||||
} else {
|
||||
console.error('Failed to load type:', result.error)
|
||||
showError('Type non trouvé')
|
||||
@@ -254,4 +170,4 @@ onMounted(async () => {
|
||||
console.log('Loading finished, loading.value:', loading.value)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user