chore: retire legacy model catalogs

This commit is contained in:
MatthieuTD
2025-10-02 16:29:50 +02:00
parent 386a1c9d1b
commit c5f2c568b6
19 changed files with 1234 additions and 3597 deletions

View File

@@ -75,7 +75,7 @@
@keydown.enter.prevent="toggleDropdown('pieces-mobile')" @keydown.enter.prevent="toggleDropdown('pieces-mobile')"
@keydown.space.prevent="toggleDropdown('pieces-mobile')" @keydown.space.prevent="toggleDropdown('pieces-mobile')"
:class=" :class="
isActive('/pieces-catalog') || isActive('/piece-category') isActive('/piece-category')
? 'bg-primary text-primary-content font-semibold shadow-sm' ? 'bg-primary text-primary-content font-semibold shadow-sm'
: 'text-base-content hover:bg-primary/10 hover:text-primary' : 'text-base-content hover:bg-primary/10 hover:text-primary'
" "
@@ -99,19 +99,6 @@
Catégorie de pièce Catégorie de pièce
</NuxtLink> </NuxtLink>
</li> </li>
<li>
<NuxtLink
to="/pieces-catalog"
class="rounded-md px-2 py-1 transition-colors"
:class="
isActive('/pieces-catalog')
? 'bg-primary/10 text-primary font-semibold'
: 'text-base-content hover:bg-primary/10 hover:text-primary'
"
>
Catalogue de pièce
</NuxtLink>
</li>
</ul> </ul>
</li> </li>
<li <li
@@ -130,7 +117,6 @@
@keydown.enter.prevent="toggleDropdown('component-mobile')" @keydown.enter.prevent="toggleDropdown('component-mobile')"
@keydown.space.prevent="toggleDropdown('component-mobile')" @keydown.space.prevent="toggleDropdown('component-mobile')"
:class=" :class="
isActive('/component-catalog') ||
isActive('/component-category') isActive('/component-category')
? 'bg-primary text-primary-content font-semibold shadow-sm' ? 'bg-primary text-primary-content font-semibold shadow-sm'
: 'text-base-content hover:bg-primary/10 hover:text-primary' : 'text-base-content hover:bg-primary/10 hover:text-primary'
@@ -155,19 +141,6 @@
Catégorie de composant Catégorie de composant
</NuxtLink> </NuxtLink>
</li> </li>
<li>
<NuxtLink
to="/component-catalog"
class="rounded-md px-2 py-1 transition-colors"
:class="
isActive('/component-catalog')
? 'bg-primary/10 text-primary font-semibold'
: 'text-base-content hover:bg-primary/10 hover:text-primary'
"
>
Catalogue de composant
</NuxtLink>
</li>
</ul> </ul>
</li> </li>
<li <li
@@ -312,13 +285,13 @@
@keydown.enter.prevent="toggleDropdown('pieces-desktop')" @keydown.enter.prevent="toggleDropdown('pieces-desktop')"
@keydown.space.prevent="toggleDropdown('pieces-desktop')" @keydown.space.prevent="toggleDropdown('pieces-desktop')"
:class=" :class="
isActive('/pieces-catalog') || isActive('/piece-category') isActive('/piece-category')
? 'bg-primary text-primary-content font-semibold shadow-sm' ? 'bg-primary text-primary-content font-semibold shadow-sm'
: 'text-base-content hover:bg-primary/10 hover:text-primary' : 'text-base-content hover:bg-primary/10 hover:text-primary'
" "
> >
Pièces Pièces
</div> </div>
<ul <ul
tabindex="0" tabindex="0"
class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-60" class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-60"
@@ -336,19 +309,6 @@
Catégorie de pièce Catégorie de pièce
</NuxtLink> </NuxtLink>
</li> </li>
<li>
<NuxtLink
to="/pieces-catalog"
class="rounded-md px-2 py-1 transition-colors"
:class="
isActive('/pieces-catalog')
? 'bg-primary/10 text-primary font-semibold'
: 'text-base-content hover:bg-primary/10 hover:text-primary'
"
>
Catalogue de pièce
</NuxtLink>
</li>
</ul> </ul>
</li> </li>
<li <li
@@ -367,14 +327,13 @@
@keydown.enter.prevent="toggleDropdown('component-desktop')" @keydown.enter.prevent="toggleDropdown('component-desktop')"
@keydown.space.prevent="toggleDropdown('component-desktop')" @keydown.space.prevent="toggleDropdown('component-desktop')"
:class=" :class="
isActive('/component-category') || isActive('/component-category')
isActive('/component-catalog') ? 'bg-primary text-primary-content font-semibold shadow-sm'
? 'bg-primary text-primary-content font-semibold shadow-sm' : 'text-base-content hover:bg-primary/10 hover:text-primary'
: 'text-base-content hover:bg-primary/10 hover:text-primary' "
" >
> Composant
Composant </div>
</div>
<ul <ul
tabindex="0" tabindex="0"
class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-64" class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-64"
@@ -392,19 +351,6 @@
Catégorie de composant Catégorie de composant
</NuxtLink> </NuxtLink>
</li> </li>
<li>
<NuxtLink
to="/component-catalog"
class="rounded-md px-2 py-1 transition-colors"
:class="
isActive('/component-catalog')
? 'bg-primary/10 text-primary font-semibold'
: 'text-base-content hover:bg-primary/10 hover:text-primary'
"
>
Catalogue de composant
</NuxtLink>
</li>
</ul> </ul>
</li> </li>
<li <li

View File

@@ -7,15 +7,9 @@
:is-edit-mode="isEditMode" :is-edit-mode="isEditMode"
:collapse-all="collapseAll" :collapse-all="collapseAll"
:toggle-token="toggleToken" :toggle-token="toggleToken"
:component-model-options="componentModelOptionsProvider(component)"
:component-model-options-provider="componentModelOptionsProvider"
:piece-model-options-provider="pieceModelOptionsProvider"
@update="$emit('update', $event)" @update="$emit('update', $event)"
@edit-piece="$emit('edit-piece', $event)" @edit-piece="$emit('edit-piece', $event)"
@assign-model="$emit('assign-model', $event)"
@assign-piece-model="$emit('assign-piece-model', $event)"
@custom-field-update="$emit('custom-field-update', $event)" @custom-field-update="$emit('custom-field-update', $event)"
@create-model-from-component="$emit('create-model-from-component', $event)"
/> />
</div> </div>
</div> </div>
@@ -40,16 +34,8 @@ defineProps({
toggleToken: { toggleToken: {
type: Number, type: Number,
default: 0 default: 0
},
componentModelOptionsProvider: {
type: Function,
default: () => []
},
pieceModelOptionsProvider: {
type: Function,
default: () => []
} }
}) })
defineEmits(['update', 'edit-piece', 'assign-model', 'assign-piece-model', 'custom-field-update', 'create-model-from-component']) defineEmits(['update', 'edit-piece', 'custom-field-update'])
</script> </script>

View File

@@ -34,12 +34,6 @@
> >
Groupe : {{ component.typeMachineComponentRequirement.label || component.typeMachineComponentRequirement.typeComposant?.name || 'Non défini' }} Groupe : {{ component.typeMachineComponentRequirement.label || component.typeMachineComponentRequirement.typeComposant?.name || 'Non défini' }}
</span> </span>
<span
v-if="component.composantModel"
class="badge badge-outline badge-sm badge-primary"
>
Modèle : {{ component.composantModel.name }}
</span>
</div> </div>
</div> </div>
</div> </div>
@@ -108,45 +102,6 @@
</div> </div>
</div> </div>
<div
v-if="isEditMode && component.typeMachineComponentRequirement"
class="grid grid-cols-1 md:grid-cols-2 gap-4"
>
<div class="form-control">
<label class="label">
<span class="label-text font-medium">Modèle de composant</span>
<span class="label-text-alt text-xs">
{{ component.typeMachineComponentRequirement.label || component.typeMachineComponentRequirement.typeComposant?.name || 'Famille' }}
</span>
</label>
<div class="flex flex-col md:flex-row gap-2 items-start md:items-center">
<select
:value="selectedComponentModelId"
class="select select-bordered select-sm"
@change="assignComponentModel($event.target.value)"
>
<option value="">
Définir manuellement
</option>
<option
v-for="model in componentModelOptionsList"
:key="model.id"
:value="model.id"
>
{{ model.name }}
</option>
</select>
<button
v-if="isEditMode && component.typeMachineComponentRequirement?.typeComposantId"
type="button"
class="btn btn-ghost btn-xs"
@click="emit('create-model-from-component', component)"
>
Sauvegarder comme modèle
</button>
</div>
</div>
</div>
</div> </div>
<!-- Custom Fields Display - Editable or Read-only --> <!-- Custom Fields Display - Editable or Read-only -->
@@ -314,7 +269,6 @@
@update="updatePiece" @update="updatePiece"
@edit="editPiece" @edit="editPiece"
@custom-field-update="updatePieceCustomField" @custom-field-update="updatePieceCustomField"
@assign-model="emitAssignPieceModel"
/> />
</div> </div>
</div> </div>
@@ -332,13 +286,9 @@
:is-edit-mode="isEditMode" :is-edit-mode="isEditMode"
:collapse-all="collapseAll" :collapse-all="collapseAll"
:toggle-token="toggleToken" :toggle-token="toggleToken"
:component-model-options="componentModelOptionsProvider(subComponent)"
:component-model-options-provider="componentModelOptionsProvider"
:piece-model-options-provider="pieceModelOptionsProvider"
@update="$emit('update', $event)" @update="$emit('update', $event)"
@edit-piece="$emit('edit-piece', $event)" @edit-piece="$emit('edit-piece', $event)"
@assign-model="$emit('assign-model', $event)" @custom-field-update="$emit('custom-field-update', $event)"
@assign-piece-model="$emit('assign-piece-model', $event)"
/> />
</div> </div>
</div> </div>
@@ -375,28 +325,13 @@ const props = defineProps({
toggleToken: { toggleToken: {
type: Number, type: Number,
default: 0 default: 0
},
componentModelOptions: {
type: Array,
default: () => []
},
componentModelOptionsProvider: {
type: Function,
default: () => []
},
pieceModelOptionsProvider: {
type: Function,
default: () => []
} }
}) })
const emit = defineEmits([ const emit = defineEmits([
'update', 'update',
'edit-piece', 'edit-piece',
'custom-field-update', 'custom-field-update'
'assign-model',
'assign-piece-model',
'create-model-from-component'
]) ])
const isCollapsed = ref(true) const isCollapsed = ref(true)
@@ -409,16 +344,19 @@ const documentIcon = doc => getFileIcon({ name: doc.filename || doc.name, mime:
const previewDocument = ref(null) const previewDocument = ref(null)
const previewVisible = ref(false) const previewVisible = ref(false)
const selectedComponentModelId = computed(() => props.component.composantModelId || props.component.composantModel?.id || '')
const componentModelOptionsList = computed(() => {
const provided = props.componentModelOptionsProvider(props.component)
return Array.isArray(provided) && provided.length ? provided : props.componentModelOptions
})
const pieceModelOptionsList = computed(() => props.pieceModelOptionsProvider(props.component) || [])
const childComponents = computed(() => { const childComponents = computed(() => {
const list = props.component.subcomponents || props.component.subComponents || [] const list = props.component.subcomponents || props.component.subComponents || []
return Array.isArray(list) ? list : [] return Array.isArray(list) ? list : []
}) })
const extractStructureCustomFields = (structure) => {
if (!structure || typeof structure !== 'object') {
return []
}
const customFields = structure.customFields
return Array.isArray(customFields) ? customFields : []
}
function fieldKeyFromNameAndType(name, type) { function fieldKeyFromNameAndType(name, type) {
const normalizedName = typeof name === 'string' ? name : '' const normalizedName = typeof name === 'string' ? name : ''
const normalizedType = typeof type === 'string' ? type : '' const normalizedType = typeof type === 'string' ? type : ''
@@ -520,24 +458,49 @@ function mergeFieldDefinitionsWithValues(definitions, values) {
return merged return merged
} }
const componentDefinitionSources = computed(() => {
const requirement = props.component.typeMachineComponentRequirement || {}
const type = requirement.typeComposant || props.component.typeComposant || {}
const definitions = []
const pushFields = (collection) => {
if (Array.isArray(collection)) {
definitions.push(...collection)
}
}
pushFields(props.component.customFields)
pushFields(props.component.definition?.customFields)
pushFields(type.customFields)
pushFields(requirement.customFields)
pushFields(requirement.definition?.customFields)
;[
props.component.definition?.structure,
type.structure,
type.componentSkeleton,
requirement.structure,
requirement.componentSkeleton,
].forEach((structure) => {
const fields = extractStructureCustomFields(structure)
if (fields.length) {
definitions.push(...fields)
}
})
return definitions
})
const displayedCustomFields = computed(() => const displayedCustomFields = computed(() =>
mergeFieldDefinitionsWithValues( mergeFieldDefinitionsWithValues(
props.component.customFields, componentDefinitionSources.value,
props.component.customFieldValues, props.component.customFieldValues,
), ),
) )
const candidateCustomFields = computed(() => {
const sources = [
props.component.customFieldValues?.map((value) => value?.customField),
props.component.typeComposant?.customFields,
props.component.typeMachineComponentRequirement?.typeComposant?.customFields,
props.component.composantModel?.customFields,
props.component.typeMachineComponentRequirement?.customFields,
props.component.customFields,
]
const candidateCustomFields = computed(() => {
const map = new Map() const map = new Map()
sources.forEach((collection) => { const register = (collection) => {
if (!Array.isArray(collection)) { if (!Array.isArray(collection)) {
return return
} }
@@ -553,7 +516,10 @@ const candidateCustomFields = computed(() => {
} }
map.set(key, item) map.set(key, item)
}) })
}) }
register(props.component.customFieldValues?.map((value) => value?.customField))
register(componentDefinitionSources.value)
return Array.from(map.values()) return Array.from(map.values())
}) })
@@ -838,25 +804,6 @@ const updatePieceCustomField = (fieldUpdate) => {
emit('edit-piece', { ...fieldUpdate, type: 'custom-field-update' }) emit('edit-piece', { ...fieldUpdate, type: 'custom-field-update' })
} }
const assignComponentModel = (value) => {
const previousModelId = props.component.composantModelId || props.component.composantModel?.id || null
const previousModel = props.component.composantModel || null
props.component.composantModelId = value || null
if (!value) {
props.component.composantModel = null
}
emit('assign-model', {
componentId: props.component.id,
composantModelId: value || null,
previousModelId,
previousModel
})
}
const emitAssignPieceModel = (payload) => {
emit('assign-piece-model', payload)
}
const ensureDocumentsLoaded = async () => { const ensureDocumentsLoaded = async () => {
if (documentsLoaded.value || !props.component?.id) { return } if (documentsLoaded.value || !props.component?.id) { return }
await refreshDocuments() await refreshDocuments()

View File

@@ -36,12 +36,6 @@
"Non défini" "Non défini"
}} }}
</span> </span>
<span
v-if="piece.pieceModel"
class="badge badge-outline badge-primary badge-sm"
>
Modèle : {{ piece.pieceModel.name }}
</span>
<span <span
v-if="piece.parentComponentName" v-if="piece.parentComponentName"
class="badge badge-ghost badge-sm" class="badge badge-ghost badge-sm"
@@ -104,33 +98,6 @@
</div> </div>
</div> </div>
<div v-if="isEditMode && piece.typeMachinePieceRequirement" class="mt-3">
<label class="label">
<span class="label-text text-sm font-medium">Modèle de pièce</span>
<span class="label-text-alt text-xs">
{{
piece.typeMachinePieceRequirement.label ||
piece.typeMachinePieceRequirement.typePiece?.name ||
"Groupe"
}}
</span>
</label>
<select
:value="selectedPieceModelId"
class="select select-bordered select-sm w-full"
@change="assignPieceModel($event.target.value)"
>
<option value="">Définir manuellement</option>
<option
v-for="model in pieceModelOptions"
:key="model.id"
:value="model.id"
>
{{ model.name }}
</option>
</select>
</div>
<!-- Champs personnalisés de la pièce --> <!-- Champs personnalisés de la pièce -->
<div <div
v-if="displayedCustomFields.length" v-if="displayedCustomFields.length"
@@ -381,17 +348,12 @@ const props = defineProps({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
pieceModelOptions: {
type: Array,
default: () => [],
},
}); });
const emit = defineEmits([ const emit = defineEmits([
"update", "update",
"edit", "edit",
"custom-field-update", "custom-field-update",
"assign-model",
]); ]);
// Données locales isolées pour cette pièce // Données locales isolées pour cette pièce
@@ -412,10 +374,13 @@ const documentIcon = (doc) =>
getFileIcon({ name: doc.filename || doc.name, mime: doc.mimeType }); getFileIcon({ name: doc.filename || doc.name, mime: doc.mimeType });
const previewDocument = ref(null); const previewDocument = ref(null);
const previewVisible = ref(false); const previewVisible = ref(false);
const selectedPieceModelId = computed( const extractStructureCustomFields = (structure) => {
() => props.piece.pieceModelId || props.piece.pieceModel?.id || "" if (!structure || typeof structure !== "object") {
); return [];
const pieceModelOptions = computed(() => props.pieceModelOptions || []); }
const customFields = structure.customFields;
return Array.isArray(customFields) ? customFields : [];
};
function fieldKeyFromNameAndType(name, type) { function fieldKeyFromNameAndType(name, type) {
const normalizedName = typeof name === 'string' ? name : ''; const normalizedName = typeof name === 'string' ? name : '';
const normalizedType = typeof type === 'string' ? type : ''; const normalizedType = typeof type === 'string' ? type : '';
@@ -529,25 +494,53 @@ function mergeFieldDefinitionsWithValues(definitions, values) {
return merged; return merged;
} }
const pieceDefinitionSources = computed(() => {
const requirement = props.piece.typeMachinePieceRequirement || {};
const type = requirement.typePiece || props.piece.typePiece || {};
const definitions = [];
const pushFields = (collection) => {
if (Array.isArray(collection)) {
definitions.push(...collection);
}
};
pushFields(props.piece.customFields);
pushFields(props.piece.definition?.customFields);
pushFields(props.piece.typePiece?.customFields);
pushFields(type.customFields);
pushFields(requirement.typePiece?.customFields);
pushFields(requirement.customFields);
pushFields(requirement.definition?.customFields);
[
props.piece.definition?.structure,
props.piece.typePiece?.structure,
type.structure,
type.pieceSkeleton,
props.piece.typePiece?.pieceSkeleton,
requirement.structure,
requirement.pieceSkeleton,
].forEach((structure) => {
const fields = extractStructureCustomFields(structure);
if (fields.length) {
definitions.push(...fields);
}
});
return definitions;
});
const displayedCustomFields = computed(() => const displayedCustomFields = computed(() =>
mergeFieldDefinitionsWithValues( mergeFieldDefinitionsWithValues(
props.piece.customFields, pieceDefinitionSources.value,
props.piece.customFieldValues, props.piece.customFieldValues,
), ),
); );
const candidateCustomFields = computed(() => { const candidateCustomFields = computed(() => {
const sources = [
props.piece.customFieldValues?.map((value) => value?.customField),
props.piece.typePiece?.customFields,
props.piece.typeMachinePieceRequirement?.typePiece?.customFields,
props.piece.typeMachinePieceRequirement?.customFields,
props.piece.pieceModel?.customFields,
props.piece.customFields,
];
const map = new Map(); const map = new Map();
sources.forEach((collection) => { const register = (collection) => {
if (!Array.isArray(collection)) { if (!Array.isArray(collection)) {
return; return;
} }
@@ -563,7 +556,10 @@ const candidateCustomFields = computed(() => {
} }
map.set(key, item); map.set(key, item);
}); });
}); };
register(props.piece.customFieldValues?.map((value) => value?.customField));
register(pieceDefinitionSources.value);
return Array.from(map.values()); return Array.from(map.values());
}); });
@@ -827,22 +823,6 @@ const updatePiece = () => {
}); });
}; };
const assignPieceModel = (value) => {
const previousModelId =
props.piece.pieceModelId || props.piece.pieceModel?.id || null;
const previousModel = props.piece.pieceModel || null;
props.piece.pieceModelId = value || null;
if (!value) {
props.piece.pieceModel = null;
}
emit("assign-model", {
pieceId: props.piece.id,
pieceModelId: value || null,
previousModelId,
previousModel,
});
};
const updateCustomFieldValue = async (fieldValueId, field) => { const updateCustomFieldValue = async (fieldValueId, field) => {
if (!field || resolveFieldReadOnly(field)) { if (!field || resolveFieldReadOnly(field)) {
return; return;

View File

@@ -1,199 +0,0 @@
<template>
<div :class="['space-y-4', depth > 0 ? 'border-l border-base-200 pl-4 ml-2' : '']">
<div class="bg-base-100 border border-base-200 rounded-lg p-4 space-y-4">
<div class="flex flex-col gap-2">
<div class="flex items-start justify-between gap-3">
<div class="space-y-1">
<h4 class="font-semibold text-sm">
{{ node.alias || node.typeComposantLabel || 'Composant' }}
</h4>
<p class="text-xs text-gray-500">
{{
node.typeComposantLabel
? `Famille : ${node.typeComposantLabel}`
: node.typeComposantId
? `Famille : ${node.typeComposantId}`
: 'Famille non définie'
}}
</p>
</div>
<div v-if="!showSelfSelector && selectedComponentModelLabel" class="badge badge-outline badge-sm">
Modèle : {{ selectedComponentModelLabel }}
</div>
</div>
<div v-if="showSelfSelector" class="form-control max-w-xs">
<label class="label">
<span class="label-text text-xs">Modèle de composant</span>
</label>
<select
class="select select-bordered select-xs"
:value="selectedComponentModelId"
@change="onComponentModelSelect($event.target.value)"
>
<option value="">Sélectionner un modèle</option>
<option v-for="model in componentModels" :key="model.id" :value="model.id">
{{ model.name }}
</option>
</select>
<p v-if="loadingComponentModels" class="text-[10px] text-gray-500 mt-1">
Chargement des modèles
</p>
<p v-else-if="!componentModels.length" class="text-[10px] text-gray-500 mt-1">
Aucun modèle disponible pour cette famille.
</p>
</div>
</div>
<div v-if="hasPieces" class="space-y-2">
<h5 class="text-[11px] font-semibold uppercase text-gray-500">Pièces associées</h5>
<div
v-for="(piece, pieceIndex) in node.pieces"
:key="pieceIndex"
class="bg-base-200/60 border border-base-200 rounded-md p-3 space-y-2"
>
<div class="space-y-1">
<span class="font-medium text-sm">{{ piece.typePieceLabel || 'Pièce' }}</span>
<p class="text-[11px] text-gray-500">
{{
piece.typePieceLabel
? `Famille : ${piece.typePieceLabel}`
: piece.typePieceId
? `Famille : ${piece.typePieceId}`
: 'Famille non définie'
}}
</p>
</div>
<div class="form-control max-w-xs">
<label class="label">
<span class="label-text text-xs">Modèle de pièce</span>
</label>
<select
class="select select-bordered select-xs"
:value="piece.__pieceModelId || ''"
@change="onPieceModelSelect(piece, $event.target.value)"
>
<option value="">Sélectionner un modèle</option>
<option v-for="model in getPieceModels(piece.typePieceId)" :key="model.id" :value="model.id">
{{ model.name }}
</option>
</select>
<p v-if="loadingPieceModels" class="text-[10px] text-gray-500 mt-1">
Chargement des modèles
</p>
<p
v-else-if="getPieceModels(piece.typePieceId).length === 0"
class="text-[10px] text-gray-500 mt-1"
>
Aucun modèle disponible pour cette famille.
</p>
</div>
</div>
</div>
<div v-if="hasSubComponents" class="space-y-3">
<h5 class="text-[11px] font-semibold uppercase text-gray-500">Sous-composants</h5>
<SkeletonComponentNodeSelector
v-for="(subComponent, index) in (node.subcomponents || node.subComponents || [])"
:key="index"
:node="subComponent"
:depth="depth + 1"
:get-component-models-for-type="getComponentModelsForType"
:get-piece-models-for-type="getPieceModelsForType"
:loading-component-models="loadingComponentModels"
:loading-piece-models="loadingPieceModels"
@component-model-change="forwardComponentModelChange"
@piece-model-change="forwardPieceModelChange"
/>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
defineOptions({ name: 'SkeletonComponentNodeSelector' })
const props = defineProps({
node: {
type: Object,
required: true,
},
depth: {
type: Number,
default: 0,
},
getComponentModelsForType: {
type: Function,
required: true,
},
getPieceModelsForType: {
type: Function,
required: true,
},
loadingComponentModels: {
type: Boolean,
default: false,
},
loadingPieceModels: {
type: Boolean,
default: false,
},
showSelfSelector: {
type: Boolean,
default: true,
},
})
const emit = defineEmits(['component-model-change', 'piece-model-change'])
const componentModels = computed(() => {
if (!props.node?.typeComposantId) {
return []
}
const models = props.getComponentModelsForType(props.node.typeComposantId)
return Array.isArray(models) ? models : []
})
const selectedComponentModelId = computed(() => props.node?.__componentModelId || '')
const selectedComponentModelLabel = computed(() => {
if (!selectedComponentModelId.value) {
return ''
}
const match = componentModels.value.find((model) => model.id === selectedComponentModelId.value)
return match?.name || ''
})
const hasPieces = computed(() => Array.isArray(props.node?.pieces) && props.node.pieces.length > 0)
const hasSubComponents = computed(() => {
const list = props.node?.subcomponents || props.node?.subComponents || []
return Array.isArray(list) && list.length > 0
})
const getPieceModels = (typePieceId) => {
if (!typePieceId) {
return []
}
const models = props.getPieceModelsForType(typePieceId)
return Array.isArray(models) ? models : []
}
const onComponentModelSelect = (value) => {
emit('component-model-change', props.node, props.node?.typeComposantId || '', value || '')
}
const onPieceModelSelect = (piece, value) => {
emit('piece-model-change', piece, piece?.typePieceId || '', value || '')
}
const forwardComponentModelChange = (...args) => {
emit('component-model-change', ...args)
}
const forwardPieceModelChange = (...args) => {
emit('piece-model-change', ...args)
}
</script>

View File

@@ -9,7 +9,7 @@
Les champs marqués d'un astérisque sont obligatoires. Les champs marqués d'un astérisque sont obligatoires.
</p> </p>
<form class="mt-6 space-y-5" @submit.prevent="handleSubmit"> <form class="mt-6 space-y-6" @submit.prevent="handleSubmit">
<div> <div>
<label class="label" for="model-type-name"> <label class="label" for="model-type-name">
<span class="label-text">Nom *</span> <span class="label-text">Nom *</span>
@@ -79,10 +79,59 @@
<p class="mt-1 text-xs text-base-content/70">Saisissez des informations complémentaires (facultatif).</p> <p class="mt-1 text-xs text-base-content/70">Saisissez des informations complémentaires (facultatif).</p>
</div> </div>
<div class="divider"></div>
<section class="space-y-4">
<header>
<h3 class="text-lg font-semibold text-base-content">
Structure du squelette
</h3>
<p class="mt-1 text-sm text-base-content/70">
Définissez la structure canonique appliquée lors de la création des composants ou pièces de cette catégorie.
</p>
</header>
<div
v-if="structureLoading"
class="flex items-center justify-center rounded-lg border border-dashed border-base-300 py-12"
>
<span class="loading loading-spinner loading-lg" aria-hidden="true"></span>
<span class="ml-3 text-sm text-base-content/70">Chargement du squelette…</span>
</div>
<template v-else>
<div
v-if="form.category === 'COMPONENT'"
class="space-y-3 rounded-lg border border-base-300 p-4"
>
<p class="text-sm text-base-content/70">
Aperçu :
<span class="font-medium text-base-content">{{ componentStructurePreview }}</span>
</p>
<ComponentModelStructureEditor v-model="componentStructure" />
</div>
<div
v-else
class="space-y-3 rounded-lg border border-base-300 p-4"
>
<p class="text-sm text-base-content/70">
Aperçu :
<span class="font-medium text-base-content">{{ pieceStructurePreview }}</span>
</p>
<PieceModelStructureEditor v-model="pieceStructure" />
</div>
</template>
</section>
<div class="modal-action"> <div class="modal-action">
<button type="button" class="btn btn-ghost" @click="close">Annuler</button> <button type="button" class="btn btn-ghost" @click="close">Annuler</button>
<button type="submit" class="btn btn-primary" :disabled="saving"> <button type="submit" class="btn btn-primary" :disabled="isSubmitDisabled">
<span v-if="saving" class="loading loading-spinner loading-sm" aria-hidden="true"></span> <span
v-if="saving"
class="loading loading-spinner loading-sm"
aria-hidden="true"
></span>
{{ submitLabel }} {{ submitLabel }}
</button> </button>
</div> </div>
@@ -95,6 +144,18 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, nextTick, reactive, ref, watch, onBeforeUnmount } from 'vue'; import { computed, nextTick, reactive, ref, watch, onBeforeUnmount } from 'vue';
import ComponentModelStructureEditor from '~/components/ComponentModelStructureEditor.vue';
import PieceModelStructureEditor from '~/components/PieceModelStructureEditor.vue';
import {
clonePieceStructure,
cloneStructure,
defaultPieceStructure,
defaultStructure,
formatPieceStructurePreview,
formatStructurePreview,
normalizePieceStructureForSave,
normalizeStructureForSave,
} from '~/shared/modelUtils';
import type { ModelCategory, ModelTypePayload } from '~/services/modelTypes'; import type { ModelCategory, ModelTypePayload } from '~/services/modelTypes';
const props = defineProps<{ const props = defineProps<{
@@ -104,6 +165,7 @@ const props = defineProps<{
initialData?: Partial<ModelTypePayload> | null; initialData?: Partial<ModelTypePayload> | null;
saving?: boolean; saving?: boolean;
lockCategory?: boolean; lockCategory?: boolean;
structureLoading?: boolean;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
@@ -112,12 +174,14 @@ const emit = defineEmits<{
}>(); }>();
const lockCategory = computed(() => props.lockCategory ?? false); const lockCategory = computed(() => props.lockCategory ?? false);
const structureLoading = computed(() => props.structureLoading ?? false);
const form = reactive<ModelTypePayload>({ const form = reactive<ModelTypePayload>({
name: '', name: '',
code: '', code: '',
category: 'COMPONENT', category: 'COMPONENT',
notes: '', notes: '',
structure: undefined,
}); });
const errors = reactive<{ name?: string; code?: string }>({}); const errors = reactive<{ name?: string; code?: string }>({});
@@ -126,6 +190,9 @@ const nameInput = ref<HTMLInputElement | null>(null);
const codePattern = /^[a-z0-9\-_.]+$/i; const codePattern = /^[a-z0-9\-_.]+$/i;
const componentStructure = ref(normalizeStructureForSave(defaultStructure()));
const pieceStructure = ref(normalizePieceStructureForSave(defaultPieceStructure()));
const resetForm = () => { const resetForm = () => {
form.name = props.initialData?.name ?? ''; form.name = props.initialData?.name ?? '';
form.code = props.initialData?.code ?? ''; form.code = props.initialData?.code ?? '';
@@ -133,6 +200,21 @@ const resetForm = () => {
form.notes = props.initialData?.notes ?? ''; form.notes = props.initialData?.notes ?? '';
errors.name = undefined; errors.name = undefined;
errors.code = undefined; errors.code = undefined;
const incomingStructure = props.initialData?.structure;
if (form.category === 'COMPONENT') {
componentStructure.value = normalizeStructureForSave(
incomingStructure && props.initialData?.category === 'COMPONENT'
? incomingStructure
: defaultStructure(),
);
} else {
pieceStructure.value = normalizePieceStructureForSave(
incomingStructure && props.initialData?.category === 'PIECE'
? incomingStructure
: defaultPieceStructure(),
);
}
}; };
const close = () => { const close = () => {
@@ -146,6 +228,7 @@ const modalTitle = computed(() =>
const submitLabel = computed(() => (props.mode === 'edit' ? 'Enregistrer' : 'Créer')); const submitLabel = computed(() => (props.mode === 'edit' ? 'Enregistrer' : 'Créer'));
const saving = computed(() => props.saving ?? false); const saving = computed(() => props.saving ?? false);
const isSubmitDisabled = computed(() => saving.value || structureLoading.value);
const onEscape = (event: KeyboardEvent) => { const onEscape = (event: KeyboardEvent) => {
if (event.key === 'Escape') { if (event.key === 'Escape') {
@@ -179,11 +262,25 @@ const handleSubmit = () => {
return; return;
} }
emit('save', { const common = {
name: form.name.trim(), name: form.name.trim(),
code: form.code.trim(), code: form.code.trim(),
category: form.category,
notes: form.notes?.trim() ? form.notes.trim() : undefined, notes: form.notes?.trim() ? form.notes.trim() : undefined,
};
if (form.category === 'COMPONENT') {
emit('save', {
...common,
category: 'COMPONENT',
structure: normalizeStructureForSave(cloneStructure(componentStructure.value)),
});
return;
}
emit('save', {
...common,
category: 'PIECE',
structure: normalizePieceStructureForSave(clonePieceStructure(pieceStructure.value)),
}); });
}; };
@@ -200,6 +297,53 @@ watch(
}, },
); );
watch(
() => form.category,
(category, previous) => {
if (category === previous) {
return;
}
if (category === 'COMPONENT' && previous !== 'COMPONENT') {
componentStructure.value = normalizeStructureForSave(defaultStructure());
}
if (category === 'PIECE' && previous !== 'PIECE') {
pieceStructure.value = normalizePieceStructureForSave(defaultPieceStructure());
}
},
);
watch(
() => props.initialData?.structure,
(value) => {
if (!props.visible) {
return;
}
if (form.category === 'COMPONENT') {
componentStructure.value = normalizeStructureForSave(
props.initialData?.category === 'COMPONENT' && value
? value
: componentStructure.value,
);
} else if (form.category === 'PIECE') {
pieceStructure.value = normalizePieceStructureForSave(
props.initialData?.category === 'PIECE' && value
? value
: pieceStructure.value,
);
}
},
);
const componentStructurePreview = computed(() =>
formatStructurePreview(componentStructure.value),
);
const pieceStructurePreview = computed(() =>
formatPieceStructurePreview(pieceStructure.value),
);
watch( watch(
() => props.initialData, () => props.initialData,
() => { () => {

View File

@@ -41,6 +41,7 @@
:initial-data="modalInitialData" :initial-data="modalInitialData"
:saving="saving" :saving="saving"
:lock-category="!allowCategorySwitch" :lock-category="!allowCategorySwitch"
:structure-loading="structureLoading"
@close="closeModal" @close="closeModal"
@save="handleSave" @save="handleSave"
/> />
@@ -56,6 +57,7 @@ import EditModal from "~/components/model-types/EditModal.vue";
import { import {
createModelType, createModelType,
deleteModelType, deleteModelType,
getModelType,
listModelTypes, listModelTypes,
updateModelType, updateModelType,
type ModelCategory, type ModelCategory,
@@ -97,6 +99,7 @@ const isModalOpen = ref(false);
const modalMode = ref<"create" | "edit">("create"); const modalMode = ref<"create" | "edit">("create");
const modalInitialData = ref<Partial<ModelTypePayload> | null>(null); const modalInitialData = ref<Partial<ModelTypePayload> | null>(null);
const editingId = ref<string | null>(null); const editingId = ref<string | null>(null);
const structureLoading = ref(false);
let debounceTimer: ReturnType<typeof setTimeout> | null = null; let debounceTimer: ReturnType<typeof setTimeout> | null = null;
let activeController: AbortController | null = null; let activeController: AbortController | null = null;
@@ -234,10 +237,11 @@ const openCreateModal = () => {
modalMode.value = "create"; modalMode.value = "create";
modalInitialData.value = null; modalInitialData.value = null;
editingId.value = null; editingId.value = null;
structureLoading.value = false;
isModalOpen.value = true; isModalOpen.value = true;
}; };
const openEditModal = (item: ModelType) => { const openEditModal = async (item: ModelType) => {
modalMode.value = "edit"; modalMode.value = "edit";
editingId.value = item.id; editingId.value = item.id;
modalInitialData.value = { modalInitialData.value = {
@@ -245,8 +249,26 @@ const openEditModal = (item: ModelType) => {
code: item.code, code: item.code,
category: item.category, category: item.category,
notes: item.notes ?? item.description ?? "", notes: item.notes ?? item.description ?? "",
structure: item.structure,
}; };
isModalOpen.value = true; isModalOpen.value = true;
structureLoading.value = true;
try {
const response = await getModelType(item.id);
modalInitialData.value = {
name: response.name,
code: response.code,
category: response.category,
notes: response.notes ?? response.description ?? "",
structure: response.structure,
};
} catch (error) {
showError(extractErrorMessage(error));
} finally {
structureLoading.value = false;
}
}; };
const closeModal = () => { const closeModal = () => {

View File

@@ -1,131 +0,0 @@
import { ref, computed } from 'vue'
import { useApi } from './useApi'
import { useToast } from './useToast'
const componentModelsBuckets = ref({})
const loadingComponentModels = ref(false)
export function useComponentModels () {
const { get, post, patch, delete: del } = useApi()
const { showSuccess, showError } = useToast()
const loadComponentModels = async (typeComposantId) => {
loadingComponentModels.value = true
try {
const query = typeComposantId ? `?typeComposantId=${encodeURIComponent(typeComposantId)}` : ''
const result = await get(`/types/composants/models${query}`)
if (result.success) {
const key = typeComposantId || '__all__'
componentModelsBuckets.value = {
...componentModelsBuckets.value,
[key]: result.data
}
}
return result
} catch (error) {
showError(`Impossible de charger les modèles de composant: ${error.message}`)
return { success: false, error: error.message }
} finally {
loadingComponentModels.value = false
}
}
const createComponentModel = async (payload) => {
loadingComponentModels.value = true
try {
const result = await post('/types/composants/models', payload)
if (result.success) {
const key = result.data?.typeComposantId || '__all__'
const bucket = componentModelsBuckets.value[key] || []
componentModelsBuckets.value = {
...componentModelsBuckets.value,
[key]: [...bucket, result.data]
}
showSuccess(`Modèle de composant "${result.data.name}" créé`)
}
return result
} catch (error) {
showError(`Erreur lors de la création du modèle de composant: ${error.message}`)
return { success: false, error: error.message }
} finally {
loadingComponentModels.value = false
}
}
const updateComponentModel = async (id, payload) => {
loadingComponentModels.value = true
try {
const result = await patch(`/types/composants/models/${id}`, payload)
if (result.success) {
const key = result.data?.typeComposantId || '__all__'
const bucket = componentModelsBuckets.value[key] || []
const updatedBucket = bucket.map(model =>
model.id === id ? result.data : model
)
componentModelsBuckets.value = {
...componentModelsBuckets.value,
[key]: updatedBucket
}
showSuccess(`Modèle de composant "${result.data.name}" mis à jour`)
}
return result
} catch (error) {
showError(`Erreur lors de la mise à jour du modèle de composant: ${error.message}`)
return { success: false, error: error.message }
} finally {
loadingComponentModels.value = false
}
}
const deleteComponentModel = async (id) => {
loadingComponentModels.value = true
try {
const result = await del(`/types/composants/models/${id}`)
if (result.success) {
const updatedBuckets = {}
for (const [key, bucket] of Object.entries(componentModelsBuckets.value)) {
updatedBuckets[key] = bucket.filter(model => model.id !== id)
}
componentModelsBuckets.value = updatedBuckets
showSuccess('Modèle de composant supprimé')
}
return result
} catch (error) {
showError(`Erreur lors de la suppression du modèle de composant: ${error.message}`)
return { success: false, error: error.message }
} finally {
loadingComponentModels.value = false
}
}
const allComponentModels = computed(() => {
return Object.values(componentModelsBuckets.value).reduce((acc, bucket) => {
bucket.forEach((model) => {
if (!acc.find(existing => existing.id === model.id)) {
acc.push(model)
}
})
return acc
}, [])
})
const getComponentModelsForType = (typeComposantId) => {
return componentModelsBuckets.value[typeComposantId] || []
}
const getComponentModels = () => allComponentModels.value
const isComponentModelLoading = () => loadingComponentModels.value
return {
componentModels: allComponentModels,
componentModelsBuckets,
loadingComponentModels,
loadComponentModels,
createComponentModel,
updateComponentModel,
deleteComponentModel,
getComponentModels,
getComponentModelsForType,
isComponentModelLoading
}
}

View File

@@ -51,7 +51,8 @@ export function useComponentTypes () {
code: payload.code || generateCodeFromName(payload.name), code: payload.code || generateCodeFromName(payload.name),
category: 'COMPONENT', category: 'COMPONENT',
notes: payload.description ?? payload.notes, notes: payload.description ?? payload.notes,
description: payload.description ?? null description: payload.description ?? null,
structure: payload.structure
}) })
const normalized = { const normalized = {
@@ -79,7 +80,8 @@ export function useComponentTypes () {
name: payload.name, name: payload.name,
description: payload.description, description: payload.description,
notes: payload.notes, notes: payload.notes,
code: payload.code code: payload.code,
structure: payload.structure
}) })
const normalized = { const normalized = {

View File

@@ -1,131 +0,0 @@
import { ref, computed } from 'vue'
import { useApi } from './useApi'
import { useToast } from './useToast'
const pieceModelsBuckets = ref({})
const loadingPieceModels = ref(false)
export function usePieceModels () {
const { get, post, patch, delete: del } = useApi()
const { showSuccess, showError } = useToast()
const loadPieceModels = async (typePieceId) => {
loadingPieceModels.value = true
try {
const query = typePieceId ? `?typePieceId=${encodeURIComponent(typePieceId)}` : ''
const result = await get(`/types/pieces/models${query}`)
if (result.success) {
const key = typePieceId || '__all__'
pieceModelsBuckets.value = {
...pieceModelsBuckets.value,
[key]: result.data
}
}
return result
} catch (error) {
showError(`Impossible de charger les modèles de pièce: ${error.message}`)
return { success: false, error: error.message }
} finally {
loadingPieceModels.value = false
}
}
const createPieceModel = async (payload) => {
loadingPieceModels.value = true
try {
const result = await post('/types/pieces/models', payload)
if (result.success) {
const key = result.data?.typePieceId || '__all__'
const bucket = pieceModelsBuckets.value[key] || []
pieceModelsBuckets.value = {
...pieceModelsBuckets.value,
[key]: [...bucket, result.data]
}
showSuccess(`Modèle de pièce "${result.data.name}" créé`)
}
return result
} catch (error) {
showError(`Erreur lors de la création du modèle de pièce: ${error.message}`)
return { success: false, error: error.message }
} finally {
loadingPieceModels.value = false
}
}
const updatePieceModel = async (id, payload) => {
loadingPieceModels.value = true
try {
const result = await patch(`/types/pieces/models/${id}`, payload)
if (result.success) {
const key = result.data?.typePieceId || '__all__'
const bucket = pieceModelsBuckets.value[key] || []
const updatedBucket = bucket.map(model =>
model.id === id ? result.data : model
)
pieceModelsBuckets.value = {
...pieceModelsBuckets.value,
[key]: updatedBucket
}
showSuccess(`Modèle de pièce "${result.data.name}" mis à jour`)
}
return result
} catch (error) {
showError(`Erreur lors de la mise à jour du modèle de pièce: ${error.message}`)
return { success: false, error: error.message }
} finally {
loadingPieceModels.value = false
}
}
const deletePieceModel = async (id) => {
loadingPieceModels.value = true
try {
const result = await del(`/types/pieces/models/${id}`)
if (result.success) {
const updatedBuckets = {}
for (const [key, bucket] of Object.entries(pieceModelsBuckets.value)) {
updatedBuckets[key] = bucket.filter(model => model.id !== id)
}
pieceModelsBuckets.value = updatedBuckets
showSuccess('Modèle de pièce supprimé')
}
return result
} catch (error) {
showError(`Erreur lors de la suppression du modèle de pièce: ${error.message}`)
return { success: false, error: error.message }
} finally {
loadingPieceModels.value = false
}
}
const allPieceModels = computed(() => {
return Object.values(pieceModelsBuckets.value).reduce((acc, bucket) => {
bucket.forEach((model) => {
if (!acc.find(existing => existing.id === model.id)) {
acc.push(model)
}
})
return acc
}, [])
})
const getPieceModelsForType = (typePieceId) => {
return pieceModelsBuckets.value[typePieceId] || []
}
const getPieceModels = () => allPieceModels.value
const isPieceModelLoading = () => loadingPieceModels.value
return {
pieceModels: allPieceModels,
pieceModelsBuckets,
loadingPieceModels,
loadPieceModels,
createPieceModel,
updatePieceModel,
deletePieceModel,
getPieceModels,
getPieceModelsForType,
isPieceModelLoading
}
}

View File

@@ -51,7 +51,8 @@ export function usePieceTypes () {
code: payload.code || generateCodeFromName(payload.name), code: payload.code || generateCodeFromName(payload.name),
category: 'PIECE', category: 'PIECE',
notes: payload.description ?? payload.notes, notes: payload.description ?? payload.notes,
description: payload.description ?? null description: payload.description ?? null,
structure: payload.structure
}) })
const normalized = { const normalized = {
@@ -79,7 +80,8 @@ export function usePieceTypes () {
name: payload.name, name: payload.name,
description: payload.description, description: payload.description,
notes: payload.notes, notes: payload.notes,
code: payload.code code: payload.code,
structure: payload.structure
}) })
const normalized = { const normalized = {

View File

@@ -1,398 +1,29 @@
<template> <template>
<main class="container mx-auto px-6 py-8 space-y-8"> <main class="container mx-auto px-6 py-12">
<header class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between"> <section class="mx-auto max-w-2xl space-y-4 rounded-2xl border border-dashed border-base-300 bg-base-100 p-8 text-center shadow-sm">
<div class="space-y-1"> <h1 class="text-3xl font-semibold text-gray-800">
<h1 class="text-3xl font-bold text-gray-800"> Le catalogue de modèles a été retiré
Catalogue de composant </h1>
</h1> <p class="text-sm text-gray-500">
<p class="text-sm text-gray-500"> Les modèles sont désormais gérés directement depuis les catégories de composants.
Gérez les modèles disponibles pour chaque famille de composant. Retrouvez tous vos squelettes et paramètres de référence dans la gestion des catégories.
</p> </p>
</div> <div class="flex flex-wrap items-center justify-center gap-3 pt-2">
<div class="tabs tabs-boxed"> <NuxtLink to="/component-category" class="btn btn-primary">
<NuxtLink to="/component-catalog" class="tab tab-active"> Ouvrir la gestion des catégories
Composants
</NuxtLink> </NuxtLink>
<NuxtLink to="/pieces-catalog" class="tab"> <NuxtLink to="/piece-category" class="btn btn-outline">
Pièces Gérer les catégories de pièces
</NuxtLink> </NuxtLink>
</div> </div>
</header> </section>
<div class="grid grid-cols-1 gap-8 xl:grid-cols-[2fr,1fr]">
<section class="card bg-base-100 shadow-lg">
<div class="card-body space-y-4">
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div class="flex flex-col gap-2 md:flex-row md:items-center md:gap-4">
<label class="form-control w-full md:w-64">
<span class="label-text text-sm">Type de composant</span>
<select v-model="selectedType" class="select select-bordered select-sm">
<option value="all">Tous les types</option>
<option
v-for="type in componentTypes"
:key="type.id"
:value="type.id"
>
{{ type.name }}
</option>
</select>
</label>
<label class="form-control">
<span class="label-text text-sm">Filtrer</span>
<input
v-model="searchQuery"
type="search"
placeholder="Rechercher un modèle..."
class="input input-bordered input-sm"
>
</label>
<span class="text-xs text-gray-500">{{ filteredModels.length }} modèle(s)</span>
</div>
<button type="button" class="btn btn-primary btn-sm w-full md:w-auto" @click="startCreate">
<IconLucidePlus class="w-4 h-4 mr-2" aria-hidden="true" />
Nouveau modèle
</button>
</div>
<div v-if="loadingComponentModels" class="flex justify-center py-16">
<span class="loading loading-spinner loading-lg" />
</div>
<div v-else-if="filteredModels.length === 0" class="py-16 text-center text-sm text-gray-500">
Aucun modèle ne correspond à ce filtre.
</div>
<div v-else class="overflow-x-auto">
<table class="table">
<thead>
<tr class="text-sm text-gray-500">
<th>Nom</th>
<th class="hidden md:table-cell">
Description
</th>
<th>Type</th>
<th class="hidden xl:table-cell">
Structure
</th>
<th class="hidden lg:table-cell">
Modifié
</th>
<th class="text-right">
Actions
</th>
</tr>
</thead>
<tbody>
<tr
v-for="model in filteredModels"
:key="model.id"
:class="{
'bg-base-200/60': form.mode === 'edit' && form.data.id === model.id
}"
>
<td>
<div class="flex items-center gap-2">
<IconLucideLayers class="w-4 h-4 text-primary" aria-hidden="true" />
<span class="font-medium">{{ model.name }}</span>
</div>
</td>
<td class="hidden md:table-cell">
{{ model.description || '—' }}
</td>
<td>{{ model.typeComposant?.name || 'Non défini' }}</td>
<td class="hidden xl:table-cell text-xs text-gray-500">
{{ formatStructurePreview(model.structure) }}
</td>
<td class="hidden lg:table-cell text-xs text-gray-500">
{{ formatFrenchDate(model.updatedAt || model.createdAt) }}
</td>
<td class="text-right space-x-2">
<button type="button" class="btn btn-sm btn-outline" @click="startEdit(model)">
Éditer
</button>
<button type="button" class="btn btn-sm btn-error" @click="confirmDelete(model)">
Supprimer
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
<aside class="card bg-base-100 shadow-lg">
<div class="card-body space-y-4">
<div class="flex items-center justify-between">
<h2 class="card-title text-lg">
{{ form.mode === 'edit' ? 'Modifier le modèle' : 'Nouveau modèle' }}
</h2>
<button
v-if="form.mode === 'edit'"
type="button"
class="btn btn-ghost btn-xs"
@click="startCreate"
>
Réinitialiser
</button>
</div>
<form class="space-y-4" @submit.prevent="handleSubmit">
<div class="form-control">
<label class="label"><span class="label-text">Nom</span></label>
<input
v-model="form.data.name"
type="text"
class="input input-bordered input-sm"
placeholder="Nom du modèle"
required
>
</div>
<div class="form-control">
<label class="label"><span class="label-text">Description</span></label>
<textarea
v-model="form.data.description"
class="textarea textarea-bordered textarea-sm"
rows="3"
placeholder="Notes optionnelles"
/>
</div>
<div class="form-control">
<label class="label"><span class="label-text">Type de composant</span></label>
<select
v-model="form.data.typeComposantId"
class="select select-bordered select-sm"
required
>
<option value="" disabled>
Sélectionner un type
</option>
<option
v-for="type in componentTypes"
:key="type.id"
:value="type.id"
>
{{ type.name }}
</option>
</select>
</div>
<div class="divider my-0">
Structure
</div>
<ComponentModelStructureEditor
v-model="form.data.structure"
:root-type-id="form.data.typeComposantId"
:root-type-label="selectedComponentTypeLabel"
:lock-root-type="!!form.data.typeComposantId"
/>
<div class="rounded-lg border border-base-200 bg-base-200/60 p-3 text-xs text-gray-500">
Aperçu : {{ formatStructurePreview(form.data.structure) }}
</div>
<div class="card-actions justify-end">
<button type="submit" class="btn btn-primary" :class="{ loading: form.submitting }">
{{ form.mode === 'edit' ? 'Enregistrer' : 'Créer' }}
</button>
</div>
</form>
</div>
</aside>
</div>
</main> </main>
</template> </template>
<script setup> <script setup lang="ts">
import { ref, computed, reactive, onMounted, watch } from 'vue' import { onMounted } from 'vue'
import { useComponentModels } from '~/composables/useComponentModels'
import { useComponentTypes } from '~/composables/useComponentTypes'
import { useToast } from '~/composables/useToast'
import ComponentModelStructureEditor from '~/components/ComponentModelStructureEditor.vue'
import {
formatStructurePreview,
defaultStructure,
cloneStructure,
normalizeStructureForSave,
} from '~/shared/modelUtils'
import { componentModelStructureValidator } from '~/shared/types/inventory'
import { formatFrenchDate } from '~/utils/date'
import IconLucidePlus from '~icons/lucide/plus'
import IconLucideLayers from '~icons/lucide/layers'
const { componentTypes, loadComponentTypes } = useComponentTypes() onMounted(() => {
const { navigateTo('/component-category', { replace: true })
componentModels,
loadComponentModels,
createComponentModel,
updateComponentModel,
deleteComponentModel,
loadingComponentModels,
getComponentModelsForType
} = useComponentModels()
const { showError, showSuccess } = useToast()
const selectedType = ref('all')
const searchQuery = ref('')
const form = reactive({
mode: 'create',
submitting: false,
data: {
id: null,
name: '',
description: '',
typeComposantId: '',
structure: defaultStructure(),
}
})
const ensureTypeSelected = () => {
if (form.data.typeComposantId && componentTypes.value.some(type => type.id === form.data.typeComposantId)) {
return
}
if (selectedType.value !== 'all' && componentTypes.value.some(type => type.id === selectedType.value)) {
form.data.typeComposantId = selectedType.value
return
}
form.data.typeComposantId = componentTypes.value[0]?.id || ''
}
const startCreate = () => {
form.mode = 'create'
form.data = {
id: null,
name: '',
description: '',
typeComposantId: selectedType.value !== 'all' ? selectedType.value : '',
structure: defaultStructure(),
}
ensureTypeSelected()
}
const startEdit = (model) => {
form.mode = 'edit'
form.data = {
id: model.id,
name: model.name,
description: model.description || '',
typeComposantId: model.typeComposantId || model.typeComposant?.id || '',
structure: cloneStructure(model.structure || defaultStructure()),
}
}
const filteredModels = computed(() => {
const list = selectedType.value === 'all'
? componentModels.value
: getComponentModelsForType(selectedType.value) || []
if (!searchQuery.value.trim()) {
return list
}
const term = searchQuery.value.toLowerCase()
return list.filter((model) => {
return (
model.name?.toLowerCase().includes(term) ||
model.description?.toLowerCase().includes(term) ||
model.typeComposant?.name?.toLowerCase().includes(term)
)
})
})
const selectedComponentType = computed(() => {
if (!form.data.typeComposantId) {
return null
}
return componentTypes.value.find((type) => type.id === form.data.typeComposantId) || null
})
const selectedComponentTypeLabel = computed(() => {
const type = selectedComponentType.value
if (!type) {
return ''
}
return type.code ? `${type.name} (${type.code})` : type.name
})
const refreshModels = async () => {
if (selectedType.value === 'all') {
await loadComponentModels()
} else {
await loadComponentModels(selectedType.value)
}
}
const handleSubmit = async () => {
if (!form.data.typeComposantId) {
showError('Veuillez sélectionner un type de composant')
return
}
form.submitting = true
try {
const normalizedStructure = normalizeStructureForSave(form.data.structure)
const validationResult = componentModelStructureValidator.safeParse(normalizedStructure)
if (!validationResult.success) {
showError(`Structure invalide: ${validationResult.issues.join(', ')}`)
return
}
if (form.mode === 'create') {
const result = await createComponentModel({
name: form.data.name.trim(),
description: form.data.description.trim() || undefined,
typeComposantId: form.data.typeComposantId,
structure: normalizedStructure,
})
if (!result.success) {
showError(result.error || 'Impossible de créer le modèle')
return
}
showSuccess(`Modèle "${result.data.name}" créé`)
} else if (form.data.id) {
const result = await updateComponentModel(form.data.id, {
name: form.data.name.trim(),
description: form.data.description.trim() || undefined,
typeComposantId: form.data.typeComposantId,
structure: normalizedStructure,
})
if (!result.success) {
showError(result.error || 'Impossible de mettre à jour le modèle')
return
}
showSuccess(`Modèle "${result.data.name}" mis à jour`)
}
await refreshModels()
startCreate()
} finally {
form.submitting = false
}
}
const confirmDelete = async (model) => {
const ok = confirm(`Supprimer le modèle "${model.name}" ?`)
if (!ok) { return }
const result = await deleteComponentModel(model.id)
if (!result.success) {
showError(result.error || 'Impossible de supprimer ce modèle')
return
}
if (form.mode === 'edit' && form.data.id === model.id) {
startCreate()
}
showSuccess('Modèle supprimé')
await refreshModels()
}
onMounted(async () => {
await Promise.all([loadComponentTypes(), loadComponentModels()])
ensureTypeSelected()
})
watch(selectedType, async () => {
await refreshModels()
if (form.mode === 'create') {
ensureTypeSelected()
}
}) })
</script> </script>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,27 +1,27 @@
<template> <template>
<div class="min-h-[60vh] flex flex-col items-center justify-center gap-6 text-center"> <div class="min-h-[60vh] flex flex-col items-center justify-center gap-6 text-center px-6">
<div class="space-y-2"> <div class="space-y-2 max-w-xl">
<h1 class="text-3xl font-semibold"> <h1 class="text-3xl font-semibold">Catalogue retiré</h1>
Gestion des catalogues
</h1>
<p class="text-sm text-gray-500"> <p class="text-sm text-gray-500">
Administrez les modèles de composants et de pièces utilisés lors de la configuration des machines. La gestion des modèles s'effectue désormais directement depuis les catégories de composants et de pièces.
Retrouvez l'intégralité des squelettes et champs personnalisés dans ces écrans dédiés.
</p> </p>
</div> </div>
<div class="flex flex-wrap items-center justify-center gap-3"> <div class="flex flex-wrap items-center justify-center gap-3">
<NuxtLink to="/component-catalog" class="btn btn-primary"> <NuxtLink to="/component-category" class="btn btn-primary">
Catalogue de composant Gestion des catégories de composants
</NuxtLink> </NuxtLink>
<NuxtLink to="/pieces-catalog" class="btn btn-outline"> <NuxtLink to="/piece-category" class="btn btn-outline">
Catalogue de pièce Gestion des catégories de pièces
</NuxtLink> </NuxtLink>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup lang="ts">
const route = useRoute() import { onMounted } from 'vue'
if (route.fullPath === '/models') {
navigateTo('/component-catalog', { replace: true }) onMounted(() => {
} navigateTo('/component-category', { replace: true })
})
</script> </script>

View File

@@ -1,363 +1,29 @@
<template> <template>
<main class="container mx-auto px-6 py-8 space-y-8"> <main class="container mx-auto px-6 py-12">
<header class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between"> <section class="mx-auto max-w-2xl space-y-4 rounded-2xl border border-dashed border-base-300 bg-base-100 p-8 text-center shadow-sm">
<div class="space-y-1"> <h1 class="text-3xl font-semibold text-gray-800">
<h1 class="text-3xl font-bold text-gray-800"> Le catalogue de pièces a été archivé
Catalogue de pièce </h1>
</h1> <p class="text-sm text-gray-500">
<p class="text-sm text-gray-500"> La configuration des pièces repose désormais sur les catégories enrichies de squelettes.
Gérez les modèles disponibles pour chaque groupe de pièces. Utilisez la gestion des catégories pour définir les structures et champs personnalisés de référence.
</p> </p>
</div> <div class="flex flex-wrap items-center justify-center gap-3 pt-2">
<div class="tabs tabs-boxed"> <NuxtLink to="/piece-category" class="btn btn-primary">
<NuxtLink to="/component-catalog" class="tab"> Gérer les catégories de pièces
Composants
</NuxtLink> </NuxtLink>
<NuxtLink to="/pieces-catalog" class="tab tab-active"> <NuxtLink to="/component-category" class="btn btn-outline">
Pièces Accéder aux catégories de composants
</NuxtLink> </NuxtLink>
</div> </div>
</header> </section>
<div class="grid grid-cols-1 gap-8 xl:grid-cols-[2fr,1fr]">
<section class="card bg-base-100 shadow-lg">
<div class="card-body space-y-4">
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div class="flex flex-col gap-2 md:flex-row md:items-center md:gap-4">
<label class="form-control w-full md:w-64">
<span class="label-text text-sm">Type de pièce</span>
<select v-model="selectedType" class="select select-bordered select-sm">
<option value="all">Tous les types</option>
<option v-for="type in pieceTypes" :key="type.id" :value="type.id">
{{ type.name }}
</option>
</select>
</label>
<label class="form-control">
<span class="label-text text-sm">Filtrer</span>
<input
v-model="searchQuery"
type="search"
placeholder="Rechercher un modèle..."
class="input input-bordered input-sm"
>
</label>
<span class="text-xs text-gray-500">{{ filteredModels.length }} modèle(s)</span>
</div>
<button type="button" class="btn btn-primary btn-sm w-full md:w-auto" @click="startCreate">
<IconLucidePlus class="w-4 h-4 mr-2" aria-hidden="true" />
Nouveau modèle
</button>
</div>
<div v-if="loadingPieceModels" class="flex justify-center py-16">
<span class="loading loading-spinner loading-lg" />
</div>
<div v-else-if="filteredModels.length === 0" class="py-16 text-center text-sm text-gray-500">
Aucun modèle ne correspond à ce filtre.
</div>
<div v-else class="overflow-x-auto">
<table class="table">
<thead>
<tr class="text-sm text-gray-500">
<th>Nom</th>
<th class="hidden md:table-cell">
Description
</th>
<th>Type</th>
<th class="hidden lg:table-cell">
Modifié
</th>
<th class="text-right">
Actions
</th>
</tr>
</thead>
<tbody>
<tr
v-for="model in filteredModels"
:key="model.id"
:class="{
'bg-base-200/60': form.mode === 'edit' && form.data.id === model.id
}"
>
<td>
<div class="flex items-center gap-2">
<IconLucidePackage class="w-4 h-4 text-secondary" aria-hidden="true" />
<span class="font-medium">{{ model.name }}</span>
</div>
</td>
<td class="hidden md:table-cell">
{{ model.description || '—' }}
</td>
<td>{{ model.typePiece?.name || 'Non défini' }}</td>
<td class="hidden lg:table-cell text-xs text-gray-500">
{{ formatFrenchDate(model.updatedAt || model.createdAt) }}
</td>
<td class="text-right space-x-2">
<button type="button" class="btn btn-sm btn-outline" @click="startEdit(model)">
Éditer
</button>
<button type="button" class="btn btn-sm btn-error" @click="confirmDelete(model)">
Supprimer
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
<aside class="card bg-base-100 shadow-lg">
<div class="card-body space-y-4">
<div class="flex items-center justify-between">
<h2 class="card-title text-lg">
{{ form.mode === 'edit' ? 'Modifier le modèle' : 'Nouveau modèle' }}
</h2>
<button
v-if="form.mode === 'edit'"
type="button"
class="btn btn-ghost btn-xs"
@click="startCreate"
>
Réinitialiser
</button>
</div>
<form class="space-y-4" @submit.prevent="handleSubmit">
<div class="form-control">
<label class="label"><span class="label-text">Nom</span></label>
<input
v-model="form.data.name"
type="text"
class="input input-bordered input-sm"
placeholder="Nom du modèle"
required
>
</div>
<div class="form-control">
<label class="label"><span class="label-text">Description</span></label>
<textarea
v-model="form.data.description"
class="textarea textarea-bordered textarea-sm"
rows="3"
placeholder="Notes optionnelles"
/>
</div>
<div class="form-control">
<label class="label"><span class="label-text">Type de pièce</span></label>
<select
v-model="form.data.typePieceId"
class="select select-bordered select-sm"
required
>
<option value="" disabled>
Sélectionner un type
</option>
<option v-for="type in pieceTypes" :key="type.id" :value="type.id">
{{ type.name }}
</option>
</select>
</div>
<div class="divider my-0">
Structure
</div>
<PieceModelStructureEditor v-model="form.data.structure" />
<div class="rounded-lg border border-base-200 bg-base-200/60 p-3 text-xs text-gray-500">
Aperçu : {{ formatStructurePreview(form.data.structure) }}
</div>
<div class="card-actions justify-end">
<button type="submit" class="btn btn-primary" :class="{ loading: form.submitting }">
{{ form.mode === 'edit' ? 'Enregistrer' : 'Créer' }}
</button>
</div>
</form>
</div>
</aside>
</div>
</main> </main>
</template> </template>
<script setup> <script setup lang="ts">
import { ref, computed, reactive, onMounted, watch } from 'vue' import { onMounted } from 'vue'
import PieceModelStructureEditor from '~/components/PieceModelStructureEditor.vue'
import { usePieceModels } from '~/composables/usePieceModels'
import { usePieceTypes } from '~/composables/usePieceTypes'
import { useToast } from '~/composables/useToast'
import { formatStructurePreview } from '~/shared/modelUtils'
import { formatFrenchDate } from '~/utils/date'
import IconLucidePlus from '~icons/lucide/plus'
import IconLucidePackage from '~icons/lucide/package'
const { pieceTypes, loadPieceTypes } = usePieceTypes() onMounted(() => {
const { navigateTo('/piece-category', { replace: true })
pieceModels,
loadPieceModels,
createPieceModel,
updatePieceModel,
deletePieceModel,
loadingPieceModels,
getPieceModelsForType
} = usePieceModels()
const { showError, showSuccess } = useToast()
const selectedType = ref('all')
const searchQuery = ref('')
const defaultStructure = () => ({ customFields: [] })
const cloneStructure = (value) => {
try {
return JSON.parse(JSON.stringify(value ?? defaultStructure()))
} catch (error) {
return defaultStructure()
}
}
const form = reactive({
mode: 'create',
submitting: false,
data: {
id: null,
name: '',
description: '',
typePieceId: '',
structure: defaultStructure()
}
})
const ensureTypeSelected = () => {
if (form.data.typePieceId && pieceTypes.value.some(type => type.id === form.data.typePieceId)) {
return
}
if (selectedType.value !== 'all' && pieceTypes.value.some(type => type.id === selectedType.value)) {
form.data.typePieceId = selectedType.value
return
}
form.data.typePieceId = pieceTypes.value[0]?.id || ''
}
const startCreate = () => {
form.mode = 'create'
form.data = {
id: null,
name: '',
description: '',
typePieceId: selectedType.value !== 'all' ? selectedType.value : '',
structure: defaultStructure()
}
ensureTypeSelected()
}
const startEdit = (model) => {
form.mode = 'edit'
form.data = {
id: model.id,
name: model.name,
description: model.description || '',
typePieceId: model.typePieceId || model.typePiece?.id || '',
structure: cloneStructure(model.structure || defaultStructure())
}
}
const filteredModels = computed(() => {
const list = selectedType.value === 'all'
? pieceModels.value
: getPieceModelsForType(selectedType.value) || []
if (!searchQuery.value.trim()) {
return list
}
const term = searchQuery.value.toLowerCase()
return list.filter((model) => {
return (
model.name?.toLowerCase().includes(term) ||
model.description?.toLowerCase().includes(term) ||
model.typePiece?.name?.toLowerCase().includes(term)
)
})
})
const refreshModels = async () => {
if (selectedType.value === 'all') {
await loadPieceModels()
} else {
await loadPieceModels(selectedType.value)
}
}
const handleSubmit = async () => {
if (!form.data.typePieceId) {
showError('Veuillez sélectionner un type de pièce')
return
}
form.submitting = true
try {
const structure = cloneStructure(form.data.structure)
if (form.mode === 'create') {
const result = await createPieceModel({
name: form.data.name.trim(),
description: form.data.description.trim() || undefined,
typePieceId: form.data.typePieceId,
structure
})
if (!result.success) {
showError(result.error || 'Impossible de créer le modèle')
return
}
showSuccess(`Modèle "${result.data.name}" créé`)
} else if (form.data.id) {
const result = await updatePieceModel(form.data.id, {
name: form.data.name.trim(),
description: form.data.description.trim() || undefined,
typePieceId: form.data.typePieceId,
structure
})
if (!result.success) {
showError(result.error || 'Impossible de mettre à jour le modèle')
return
}
showSuccess(`Modèle "${result.data.name}" mis à jour`)
}
await refreshModels()
startCreate()
} finally {
form.submitting = false
}
}
const confirmDelete = async (model) => {
const ok = confirm(`Supprimer le modèle "${model.name}" ?`)
if (!ok) { return }
const result = await deletePieceModel(model.id)
if (!result.success) {
showError(result.error || 'Impossible de supprimer ce modèle')
return
}
if (form.mode === 'edit' && form.data.id === model.id) {
startCreate()
}
showSuccess('Modèle supprimé')
await refreshModels()
}
onMounted(async () => {
await Promise.all([loadPieceTypes(), loadPieceModels()])
ensureTypeSelected()
})
watch(selectedType, async () => {
await refreshModels()
if (form.mode === 'create') {
ensureTypeSelected()
}
}) })
</script> </script>

View File

@@ -1,20 +1,39 @@
import { useRequestFetch } from '#imports'; import { useRequestFetch } from '#imports';
import type { FetchOptions } from 'ofetch'; import type { FetchOptions } from 'ofetch';
import type {
ComponentModelStructure,
PieceModelStructure,
} from '~/shared/types/inventory';
export type ModelCategory = 'COMPONENT' | 'PIECE'; export type ModelCategory = 'COMPONENT' | 'PIECE';
export interface ModelTypePayload { export type ModelTypeStructure = ComponentModelStructure | PieceModelStructure | null;
export interface BaseModelTypePayload {
name: string; name: string;
code: string; code: string;
category: ModelCategory;
notes?: string | null; notes?: string | null;
description?: string | null; description?: string | null;
} }
export interface ModelType extends ModelTypePayload { export interface ComponentModelTypePayload extends BaseModelTypePayload {
category: 'COMPONENT';
structure?: ComponentModelStructure | null;
}
export interface PieceModelTypePayload extends BaseModelTypePayload {
category: 'PIECE';
structure?: PieceModelStructure | null;
}
export type ModelTypePayload = ComponentModelTypePayload | PieceModelTypePayload;
export interface ModelType extends BaseModelTypePayload {
id: string; id: string;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
category: ModelCategory;
structure: ModelTypeStructure;
} }
export interface ModelTypeListParams { export interface ModelTypeListParams {

View File

@@ -4,6 +4,11 @@ import {
type ComponentModelPiece, type ComponentModelPiece,
type ComponentModelStructure, type ComponentModelStructure,
type ComponentModelStructureNode, type ComponentModelStructureNode,
type PieceModelCustomField,
type PieceModelStructure,
type PieceModelStructureEditorField,
type PieceModelStructureForEditor,
createEmptyPieceModelStructure,
} from './types/inventory' } from './types/inventory'
export const isPlainObject = (value: unknown): value is Record<string, unknown> => { export const isPlainObject = (value: unknown): value is Record<string, unknown> => {
@@ -381,3 +386,126 @@ export const formatStructurePreview = (structure: any) => {
if (stats.subcomponents) segments.push(`${stats.subcomponents} sous-composant(s)`) if (stats.subcomponents) segments.push(`${stats.subcomponents} sous-composant(s)`)
return segments.join(' • ') return segments.join(' • ')
} }
export const defaultPieceStructure = (): PieceModelStructure => ({
...createEmptyPieceModelStructure(),
})
const ensurePieceStructureShape = (input: any): PieceModelStructure => {
const base = createEmptyPieceModelStructure()
if (!isPlainObject(input)) {
return base
}
const clone: PieceModelStructure = {
...base,
customFields: Array.isArray((input as any).customFields) ? (input as any).customFields : [],
}
for (const [key, value] of Object.entries(input as Record<string, unknown>)) {
if (key === 'customFields') {
continue
}
clone[key] = value
}
return clone
}
export const clonePieceStructure = (input: any): PieceModelStructure => {
try {
const cloned = JSON.parse(JSON.stringify(input ?? defaultPieceStructure()))
return ensurePieceStructureShape(cloned)
} catch (error) {
return defaultPieceStructure()
}
}
const sanitizePieceCustomFields = (fields: any[]): PieceModelCustomField[] => {
if (!Array.isArray(fields)) {
return []
}
return fields
.map((field) => {
const name = typeof field?.name === 'string' ? field.name.trim() : ''
if (!name) {
return null
}
const type = typeof field?.type === 'string' && field.type ? field.type : 'text'
const required = !!field?.required
let options: string[] | undefined
if (type === 'select') {
const rawOptions = typeof field?.optionsText === 'string'
? field.optionsText
: Array.isArray(field?.options)
? field.options.join('\n')
: ''
const parsed = rawOptions
.split(/\r?\n/)
.map((option) => option.trim())
.filter((option) => option.length > 0)
options = parsed.length > 0 ? parsed : undefined
}
const result: PieceModelCustomField = { name, type, required }
if (options) {
result.options = options
}
return result
})
.filter((field): field is PieceModelCustomField => !!field)
}
export const normalizePieceStructureForSave = (input: any): PieceModelStructure => {
const source = clonePieceStructure(input)
return {
...Object.fromEntries(
Object.entries(source).filter(([key]) => key !== 'customFields'),
),
customFields: sanitizePieceCustomFields(source.customFields),
}
}
const hydratePieceCustomFields = (fields: any[]): PieceModelStructureEditorField[] => {
if (!Array.isArray(fields)) {
return []
}
return fields.map((field) => ({
name: field?.name ?? '',
type: field?.type ?? 'text',
required: !!field?.required,
options: Array.isArray(field?.options) ? field.options : undefined,
optionsText: Array.isArray(field?.options) ? field.options.join('\n') : (field?.optionsText ?? ''),
}))
}
export const hydratePieceStructureForEditor = (input: any): PieceModelStructureForEditor => {
const source = clonePieceStructure(input)
const payload: PieceModelStructureForEditor = {
...Object.fromEntries(
Object.entries(source).filter(([key]) => key !== 'customFields'),
),
customFields: hydratePieceCustomFields(source.customFields),
}
return payload
}
export const formatPieceStructurePreview = (structure: any) => {
if (!structure || typeof structure !== 'object') {
return 'Aucun champ personnalisé'
}
const customFields = Array.isArray((structure as any).customFields)
? (structure as any).customFields.length
: 0
if (!customFields) {
return 'Aucun champ personnalisé'
}
return `${customFields} champ(s) personnalisé(s)`
}

View File

@@ -27,6 +27,29 @@ export interface ComponentModelStructure extends ComponentModelStructureNode {
pieces: ComponentModelPiece[] pieces: ComponentModelPiece[]
} }
export type PieceModelCustomFieldType = ComponentModelCustomFieldType
export interface PieceModelCustomField {
name: string
type: PieceModelCustomFieldType
required: boolean
options?: string[]
}
export interface PieceModelStructure {
customFields: PieceModelCustomField[]
[key: string]: unknown
}
export interface PieceModelStructureEditorField extends PieceModelCustomField {
optionsText: string
}
export interface PieceModelStructureForEditor {
customFields: PieceModelStructureEditorField[]
[key: string]: unknown
}
const FIELD_TYPES: ComponentModelCustomFieldType[] = ['text', 'number', 'select', 'boolean', 'date'] const FIELD_TYPES: ComponentModelCustomFieldType[] = ['text', 'number', 'select', 'boolean', 'date']
const isPlainObject = (value: unknown): value is Record<string, unknown> => { const isPlainObject = (value: unknown): value is Record<string, unknown> => {
@@ -219,3 +242,7 @@ export const createEmptyComponentModelStructure = (): ComponentModelStructure =>
pieces: [], pieces: [],
subcomponents: [], subcomponents: [],
}) })
export const createEmptyPieceModelStructure = (): PieceModelStructure => ({
customFields: [],
})