set up new view for skeleton hiearchi

This commit is contained in:
Matthieu
2025-09-22 08:34:05 +02:00
parent e33e91ee26
commit 936a9d74ca
30 changed files with 4530 additions and 2288 deletions

View File

@@ -51,13 +51,13 @@
</div>
<p class="text-sm text-gray-600 line-clamp-3">{{ type.description || 'Aucune description' }}</p>
<div class="text-xs text-gray-500 flex items-center gap-2">
<span class="inline-flex items-center gap-1">
<span class="inline-flex items-center gap-1">
<IconLucideClipboardList class="h-4 w-4" aria-hidden="true" />
{{ type.components?.length || 0 }} composant(s)
{{ type.componentRequirements?.length || 0 }} famille(s)
</span>
<span class="inline-flex items-center gap-1">
<IconLucideList class="h-4 w-4" aria-hidden="true" />
{{ type.machinePieces?.length || 0 }} pièce(s) machine
{{ type.pieceRequirements?.length || 0 }} groupe(s) de pièces
</span>
</div>
</div>
@@ -93,8 +93,8 @@ const createEmptyType = () => ({
category: '',
maintenanceFrequency: '',
customFields: [],
machinePieces: [],
components: []
componentRequirements: [],
pieceRequirements: [],
})
const draftType = ref(createEmptyType())
@@ -141,36 +141,36 @@ const normalizeCustomFields = (fields = []) =>
options: parseOptions(field)
}))
const normalizePrice = (value) => {
if (value === undefined || value === null || value === '') return null
const num = Number(value)
return Number.isFinite(num) ? num : null
const toIntegerOrNull = (value, fallback = null) => {
if (value === '' || value === undefined || value === null) {
return fallback
}
const parsed = Number(value)
return Number.isFinite(parsed) ? parsed : fallback
}
const normalizePieces = (pieces = []) =>
pieces
.filter(piece => piece?.name && piece.name.trim() !== '')
.map(piece => ({
name: piece.name,
reference: piece.reference || '',
constructeur: piece.constructeur || '',
emplacement: piece.emplacement || '',
prix: normalizePrice(piece.prix),
customFields: normalizeCustomFields(piece.customFields || [])
const normalizeComponentRequirements = (requirements = []) =>
requirements
.filter(req => req?.typeComposantId)
.map(req => ({
typeComposantId: req.typeComposantId,
label: req.label?.trim() ? req.label.trim() : undefined,
minCount: toIntegerOrNull(req.minCount, 1),
maxCount: toIntegerOrNull(req.maxCount, null),
required: req.required ?? true,
allowNewModels: req.allowNewModels ?? true,
}))
const normalizeComponents = (components = []) =>
components
.filter(component => component?.name && component.name.trim() !== '')
.map(component => ({
name: component.name,
reference: component.reference || '',
constructeur: component.constructeur || '',
emplacement: component.emplacement || '',
prix: normalizePrice(component.prix),
customFields: normalizeCustomFields(component.customFields || []),
pieces: normalizePieces(component.pieces || []),
subComponents: normalizeComponents(component.subComponents || [])
const normalizePieceRequirements = (requirements = []) =>
requirements
.filter(req => req?.typePieceId)
.map(req => ({
typePieceId: req.typePieceId,
label: req.label?.trim() ? req.label.trim() : undefined,
minCount: toIntegerOrNull(req.minCount, 0),
maxCount: toIntegerOrNull(req.maxCount, null),
required: req.required ?? false,
allowNewModels: req.allowNewModels ?? true,
}))
const buildPayload = (typeData) => ({
@@ -179,8 +179,8 @@ const buildPayload = (typeData) => ({
category: typeData.category,
maintenanceFrequency: typeData.maintenanceFrequency,
customFields: normalizeCustomFields(typeData.customFields),
machinePieces: normalizePieces(typeData.machinePieces),
components: normalizeComponents(typeData.components)
componentRequirements: normalizeComponentRequirements(typeData.componentRequirements),
pieceRequirements: normalizePieceRequirements(typeData.pieceRequirements)
})
const resetForm = () => {

View File

@@ -372,12 +372,12 @@
<h4 class="font-semibold text-sm mb-2">Structure du type sélectionné :</h4>
<div class="text-xs space-y-1">
<div class="flex items-center gap-2">
<span class="font-medium">Composants :</span>
<span class="badge badge-sm">{{ selectedMachineType.components?.length || 0 }}</span>
<span class="font-medium">Familles de composants :</span>
<span class="badge badge-sm">{{ selectedMachineType.componentRequirements?.length || 0 }}</span>
</div>
<div class="flex items-center gap-2">
<span class="font-medium">Pièces critiques :</span>
<span class="badge badge-sm">{{ selectedMachineType.criticalParts?.length || 0 }}</span>
<span class="font-medium">Groupes de pièces :</span>
<span class="badge badge-sm">{{ selectedMachineType.pieceRequirements?.length || 0 }}</span>
</div>
<div class="flex items-center gap-2">
<span class="font-medium">Catégorie :</span>

View File

@@ -300,6 +300,99 @@
</div>
</div>
<!-- Requirement Summary -->
<div
v-if="componentRequirementGroups.length || pieceRequirementGroups.length"
class="card bg-base-100 shadow-lg"
>
<div class="card-body space-y-6">
<div>
<h2 class="card-title">Structure sélectionnée</h2>
<p class="text-sm text-gray-500">
Synthèse des familles définies dans le type et des modèles utilisés pour cette machine.
</p>
</div>
<div v-if="componentRequirementGroups.length" class="space-y-4">
<h3 class="text-sm font-semibold text-gray-700">Composants</h3>
<div
v-for="group in componentRequirementGroups"
:key="group.requirement.id"
class="rounded-lg border border-base-200 p-4"
>
<div class="flex flex-wrap items-center justify-between gap-2 mb-3">
<div>
<h4 class="font-medium text-sm">
{{ group.requirement.label || group.requirement.typeComposant?.name || 'Famille de composants' }}
</h4>
<p class="text-xs text-gray-500">
Type : {{ group.requirement.typeComposant?.name || 'Non défini' }} · Min {{ group.requirement.minCount ?? (group.requirement.required ? 1 : 0) }} · Max {{ group.requirement.maxCount ?? '∞' }}
</p>
</div>
<span class="badge badge-outline badge-sm">{{ group.components.length }} composant(s)</span>
</div>
<div v-if="group.components.length" class="space-y-2">
<div
v-for="component in group.components"
:key="component.id"
class="flex flex-wrap items-center gap-2 text-sm"
>
<span class="font-medium">{{ component.name }}</span>
<span v-if="component.composantModel" class="badge badge-sm badge-primary badge-outline">
Modèle : {{ component.composantModel.name }}
</span>
<span v-else class="badge badge-sm badge-outline">Défini manuellement</span>
<span v-if="component.parentComposantId" class="text-xs text-gray-500">
(Sous-composant)
</span>
</div>
</div>
<p v-else class="text-xs text-gray-500">Aucun composant rattaché à ce groupe.</p>
</div>
</div>
<div v-if="pieceRequirementGroups.length" class="space-y-4">
<h3 class="text-sm font-semibold text-gray-700">Pièces principales</h3>
<div
v-for="group in pieceRequirementGroups"
:key="group.requirement.id"
class="rounded-lg border border-base-200 p-4"
>
<div class="flex flex-wrap items-center justify-between gap-2 mb-3">
<div>
<h4 class="font-medium text-sm">
{{ group.requirement.label || group.requirement.typePiece?.name || 'Groupe de pièces' }}
</h4>
<p class="text-xs text-gray-500">
Type : {{ group.requirement.typePiece?.name || 'Non défini' }} · Min {{ group.requirement.minCount ?? (group.requirement.required ? 1 : 0) }} · Max {{ group.requirement.maxCount ?? '∞' }}
</p>
</div>
<span class="badge badge-outline badge-sm">{{ group.pieces.length }} pièce(s)</span>
</div>
<div v-if="group.pieces.length" class="space-y-2">
<div
v-for="piece in group.pieces"
:key="piece.id"
class="flex flex-wrap items-center gap-2 text-sm"
>
<span class="font-medium">{{ piece.name }}</span>
<span v-if="piece.pieceModel" class="badge badge-sm badge-primary badge-outline">
Modèle : {{ piece.pieceModel.name }}
</span>
<span v-else class="badge badge-sm badge-outline">Définie manuellement</span>
<span v-if="piece.parentComponentName" class="text-xs text-gray-500">
(Rattachée à {{ piece.parentComponentName }})
</span>
</div>
</div>
<p v-else class="text-xs text-gray-500">Aucune pièce rattachée à ce groupe.</p>
</div>
</div>
</div>
</div>
<!-- Components Section -->
<div class="card bg-base-100 shadow-lg">
<div class="card-body">
@@ -327,8 +420,14 @@
:is-edit-mode="isEditMode"
:collapse-all="componentsCollapsed"
:toggle-token="collapseToggleToken"
:component-model-options-provider="getComponentModelOptions"
:piece-model-options-provider="getPieceModelOptions"
@update="updateComponent"
@edit-piece="updatePieceFromComponent"
@assign-model="assignComponentModel"
@assign-piece-model="assignPieceModel"
@custom-field-update="updatePieceCustomField"
@create-model-from-component="openSaveComponentModelModal"
/>
</div>
</div>
@@ -346,9 +445,11 @@
:key="piece.id"
:piece="piece"
:is-edit-mode="isEditMode"
:piece-model-options="getPieceModelOptions(piece)"
@update="updatePieceInfo"
@edit="editPiece"
@custom-field-update="updatePieceCustomField"
@assign-model="assignPieceModel"
/>
</div>
</div>
@@ -378,7 +479,73 @@
@select-all="setAllPrintSelection(true)"
@deselect-all="setAllPrintSelection(false)"
/>
</template>
<div v-if="saveComponentAsModelModal.open" class="modal modal-open">
<div class="modal-box max-w-2xl">
<h3 class="font-bold text-lg">
Enregistrer « {{ saveComponentAsModelModal.component?.name || 'Composant' }} » comme modèle
</h3>
<p class="text-xs text-gray-500 mb-4">
Le modèle sera associé au type {{ saveComponentAsModelModal.typeLabel || 'de composant' }} et
pourra être réutilisé lors de la configuration d'autres machines.
</p>
<form class="space-y-4" @submit.prevent="submitSaveComponentModelModal">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="form-control">
<label class="label"><span class="label-text">Nom du modèle</span></label>
<input
v-model="saveComponentAsModelModal.form.name"
type="text"
class="input input-bordered input-sm"
required
/>
</div>
<div class="form-control">
<label class="label"><span class="label-text">Type de composant</span></label>
<select
v-model="saveComponentAsModelModal.form.typeComposantId"
class="select select-bordered select-sm"
required
disabled
>
<option :value="saveComponentAsModelModal.form.typeComposantId">
{{ saveComponentAsModelModal.typeLabel || 'Type de composant' }}
</option>
</select>
</div>
</div>
<div class="form-control">
<label class="label"><span class="label-text">Description</span></label>
<textarea
v-model="saveComponentAsModelModal.form.description"
class="textarea textarea-bordered textarea-sm"
rows="3"
placeholder="Notes optionnelles"
></textarea>
</div>
<div class="bg-base-200/60 border border-base-200 rounded-lg p-3 space-y-3">
<div class="flex flex-wrap items-center gap-2 text-xs text-gray-500">
<span class="badge badge-outline badge-sm">{{ saveComponentStructureSummary }}</span>
</div>
<ModelStructureViewer :structure="saveComponentAsModelModal.structure" />
</div>
<div class="modal-action">
<button type="button" class="btn btn-outline" @click="closeSaveComponentModelModal">
Annuler
</button>
<button type="submit" class="btn btn-primary" :class="{ loading: saveComponentAsModelModal.submitting }">
Sauvegarder et assigner
</button>
</div>
</form>
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, computed, watch, nextTick } from 'vue'
@@ -405,6 +572,14 @@ import IconLucideSquarePen from '~icons/lucide/square-pen'
import IconLucideEye from '~icons/lucide/eye'
import IconLucideChevronRight from '~icons/lucide/chevron-right'
import IconLucidePrinter from '~icons/lucide/printer'
import ModelStructureViewer from '~/components/ModelStructureViewer.vue'
import { useComponentModels } from '~/composables/useComponentModels'
import { usePieceModels } from '~/composables/usePieceModels'
import {
defaultStructure,
extractStructureFromComponent,
formatStructurePreview,
} from '~/shared/modelUtils'
const route = useRoute()
const machineId = route.params.id
@@ -433,6 +608,16 @@ const {
loadDocumentsByComponent,
loadDocumentsByPiece
} = useDocuments()
const {
loadComponentModels,
getComponentModelsForType,
createComponentModel,
} = useComponentModels()
const {
loadPieceModels,
getPieceModelsForType,
} = usePieceModels()
const toast = useToast()
// Data
const loading = ref(true)
@@ -473,11 +658,62 @@ const printSelection = reactive({
pieces: {},
})
const saveComponentAsModelModal = reactive({
open: false,
submitting: false,
component: null,
typeLabel: '',
structure: defaultStructure(),
form: {
name: '',
description: '',
typeComposantId: '',
},
})
const saveComponentStructureSummary = computed(() =>
formatStructurePreview(saveComponentAsModelModal.structure)
)
const handleMachineConstructeurChange = async (value) => {
machineConstructeurId.value = value
await updateMachineInfo()
}
const openSaveComponentModelModal = (component) => {
if (!component) return
const requirement = component.typeMachineComponentRequirement || null
const typeComposantId = requirement?.typeComposantId
|| component.typeComposantId
|| component.typeComposant?.id
|| ''
saveComponentAsModelModal.open = true
saveComponentAsModelModal.submitting = false
saveComponentAsModelModal.component = component
saveComponentAsModelModal.typeLabel = requirement?.typeComposant?.name
|| component.typeComposant?.name
|| 'Composant'
saveComponentAsModelModal.form = {
name: component.name || '',
description: component.description || '',
typeComposantId,
}
saveComponentAsModelModal.structure = extractStructureFromComponent(component)
}
const closeSaveComponentModelModal = () => {
saveComponentAsModelModal.open = false
saveComponentAsModelModal.component = null
saveComponentAsModelModal.typeLabel = ''
saveComponentAsModelModal.structure = defaultStructure()
saveComponentAsModelModal.form = {
name: '',
description: '',
typeComposantId: '',
}
}
// Mode d'édition
const isEditMode = ref(false)
const debug = ref(false) // Ajout de debug pour afficher les infos de debug
@@ -521,16 +757,178 @@ const machinePieces = computed(() => {
const machineDocumentsList = computed(() => machine.value?.documents || [])
const documentIcon = (doc) => getFileIcon({ name: doc.filename || doc.name, mime: doc.mimeType })
const allComponents = computed(() => {
return components.value
})
const subComponents = (parentId) => {
return components.value.filter(comp => comp.parentComposantId === parentId)
const flattenComponents = (list = []) => {
const result = []
const traverse = (items) => {
items.forEach((item) => {
result.push(item)
if (item.subComponents && item.subComponents.length) {
traverse(item.subComponents)
}
})
}
traverse(list)
return result
}
const componentPieces = (composantId) => {
return pieces.value.filter(piece => piece.composantId === composantId)
const flattenedComponents = computed(() => flattenComponents(components.value))
const preloadModelsForTypeMachine = async (typeMachine) => {
if (!typeMachine) return
const componentTypeIds = new Set(
(typeMachine.componentRequirements || [])
.map((requirement) => requirement.typeComposantId)
.filter(Boolean),
)
const pieceTypeIds = new Set(
(typeMachine.pieceRequirements || [])
.map((requirement) => requirement.typePieceId)
.filter(Boolean),
)
await Promise.all([
...Array.from(componentTypeIds).map((id) => loadComponentModels(id)),
...Array.from(pieceTypeIds).map((id) => loadPieceModels(id)),
])
}
const ensureModelsForExistingEntities = async () => {
const componentTypeIds = new Set()
const gatherComponentTypes = (items = []) => {
items.forEach((item) => {
const typeId = item.typeMachineComponentRequirement?.typeComposantId
|| item.typeComposantId
|| item.typeComposant?.id
if (typeId) {
componentTypeIds.add(typeId)
}
if (item.subComponents?.length) {
gatherComponentTypes(item.subComponents)
}
})
}
gatherComponentTypes(components.value)
const pieceTypeIds = new Set()
pieces.value.forEach((piece) => {
const typeId = piece.typeMachinePieceRequirement?.typePieceId
|| piece.typePieceId
|| piece.typePiece?.id
if (typeId) {
pieceTypeIds.add(typeId)
}
})
await Promise.all([
...Array.from(componentTypeIds).map((id) => loadComponentModels(id)),
...Array.from(pieceTypeIds).map((id) => loadPieceModels(id)),
])
}
const componentRequirementGroups = computed(() => {
const requirements = machine.value?.typeMachine?.componentRequirements || []
if (!requirements.length) return []
const groups = requirements.map((requirement) => ({
requirement,
components: [],
}))
const map = new Map(groups.map((group) => [group.requirement.id, group]))
flattenedComponents.value.forEach((component) => {
const reqId = component.typeMachineComponentRequirementId
if (reqId && map.has(reqId)) {
map.get(reqId).components.push(component)
}
})
return groups
})
const pieceRequirementGroups = computed(() => {
const requirements = machine.value?.typeMachine?.pieceRequirements || []
if (!requirements.length) return []
const groups = requirements.map((requirement) => ({
requirement,
pieces: [],
}))
const map = new Map(groups.map((group) => [group.requirement.id, group]))
const collectPieces = () => {
const collected = []
// Pièces rattachées à la machine directement
machinePieces.value.forEach((piece) => {
collected.push({ ...piece, parentComponentName: null })
})
// Pièces rattachées aux composants
flattenedComponents.value.forEach((component) => {
if (component.pieces && component.pieces.length) {
component.pieces.forEach((piece) => {
collected.push({ ...piece, parentComponentName: component.name })
})
}
})
return collected
}
collectPieces().forEach((piece) => {
const reqId = piece.typeMachinePieceRequirementId
if (reqId && map.has(reqId)) {
map.get(reqId).pieces.push(piece)
}
})
return groups
})
const getComponentModelOptions = (entity) => {
const requirement = entity?.typeMachineComponentRequirement
const typeId = requirement?.typeComposantId
|| entity?.typeComposantId
|| entity?.typeComposant?.id
if (!typeId) return []
return getComponentModelsForType(typeId)
}
const getPieceModelOptions = (entity) => {
const requirement = entity?.typeMachinePieceRequirement
const typeId = requirement?.typePieceId
|| entity?.typePieceId
|| entity?.typePiece?.id
if (!typeId) return []
return getPieceModelsForType(typeId)
}
const findComponentById = (items, id) => {
for (const item of items || []) {
if (item.id === id) return item
const found = findComponentById(item.subComponents, id)
if (found) return found
}
return null
}
const findPieceById = (pieceId) => {
const direct = pieces.value.find((piece) => piece.id === pieceId)
if (direct) return direct
const searchInComponents = (items) => {
for (const item of items || []) {
const match = (item.pieces || []).find((piece) => piece.id === pieceId)
if (match) return match
const nested = searchInComponents(item.subComponents)
if (nested) return nested
}
return null
}
return searchInComponents(components.value)
}
const refreshMachineDocuments = async () => {
@@ -756,7 +1154,12 @@ const transformComponentCustomFields = (componentsData) => {
})) || [];
// Transform pieces for the current component
const pieces = component.pieces ? transformCustomFields(component.pieces) : [];
const pieces = component.pieces
? transformCustomFields(component.pieces).map((piece) => ({
...piece,
parentComponentName: component.name,
}))
: [];
// Recursively transform sub-components (using 'sousComposants' from backend)
const subComponents = component.sousComposants ? transformComponentCustomFields(component.sousComposants) : [];
@@ -798,6 +1201,8 @@ const loadMachineData = async () => {
machine.value.documents = machine.value.documents || []
machineDocumentsLoaded.value = !!(machine.value.documents?.length)
console.log('Machine trouvée et assignée:', machine.value)
await preloadModelsForTypeMachine(machine.value.typeMachine)
} else {
console.error('Machine non trouvée:', machineId)
console.error('Erreur API:', machineResult.error)
@@ -833,6 +1238,7 @@ const loadMachineData = async () => {
}
})
console.log('=== FIN HIÉRARCHIE ===')
await ensureModelsForExistingEntities()
} else {
console.error('Erreur lors du chargement des composants:', componentsResult.error)
}
@@ -843,6 +1249,7 @@ const loadMachineData = async () => {
pieces.value = transformCustomFields(machine.value.pieces)
console.log('Pièces transformées:', pieces.value)
console.log('Pièces chargées:', pieces.value.length)
await ensureModelsForExistingEntities()
} else {
console.log('Aucune pièce trouvée dans la réponse de la machine')
}
@@ -887,10 +1294,12 @@ const updateComponent = async (updatedComponent) => {
reference: updatedComponent.reference,
constructeurId: updatedComponent.constructeurId || updatedComponent.constructeur?.id || null,
emplacement: updatedComponent.emplacement,
prix: prixValue && prixValue !== '' ? parseFloat(prixValue) : null
prix: prixValue && prixValue !== '' ? parseFloat(prixValue) : null,
composantModelId: updatedComponent.composantModelId || updatedComponent.composantModel?.id || null,
})
if (result.success) {
Object.assign(updatedComponent, result.data)
const transformed = transformComponentCustomFields([result.data])[0]
Object.assign(updatedComponent, transformed)
}
} catch (error) {
console.error('Erreur lors de la mise à jour du composant:', error)
@@ -904,10 +1313,12 @@ const updatePieceFromComponent = async (updatedPiece) => {
reference: updatedPiece.reference,
constructeurId: updatedPiece.constructeurId || updatedPiece.constructeur?.id || null,
emplacement: updatedPiece.emplacement,
prix: updatedPiece.prix && updatedPiece.prix !== '' ? parseFloat(updatedPiece.prix) : null
prix: updatedPiece.prix && updatedPiece.prix !== '' ? parseFloat(updatedPiece.prix) : null,
pieceModelId: updatedPiece.pieceModelId || updatedPiece.pieceModel?.id || null,
})
if (result.success) {
Object.assign(updatedPiece, result.data)
const transformed = transformCustomFields([result.data])[0]
Object.assign(updatedPiece, transformed)
// Si la pièce a des champs personnalisés mis à jour, les traiter
if (updatedPiece.customFields) {
for (const field of updatedPiece.customFields) {
@@ -934,16 +1345,131 @@ const updatePieceInfo = async (updatedPiece) => {
reference: updatedPiece.reference,
constructeurId: updatedPiece.constructeurId || updatedPiece.constructeur?.id || null,
emplacement: updatedPiece.emplacement,
prix: updatedPiece.prix && updatedPiece.prix !== '' ? parseFloat(updatedPiece.prix) : null
prix: updatedPiece.prix && updatedPiece.prix !== '' ? parseFloat(updatedPiece.prix) : null,
pieceModelId: updatedPiece.pieceModelId || updatedPiece.pieceModel?.id || null,
})
if (result.success) {
Object.assign(updatedPiece, result.data)
const transformed = transformCustomFields([result.data])[0]
Object.assign(updatedPiece, transformed)
}
} catch (error) {
console.error('Erreur lors de la mise à jour de la pièce:', error)
}
}
const assignComponentModel = async ({ componentId, composantModelId, previousModelId, previousModel }) => {
if (!componentId) return
try {
const result = await updateComposantApi(componentId, {
composantModelId: composantModelId || null,
})
if (result.success) {
const transformed = transformComponentCustomFields([result.data])[0]
const target = findComponentById(components.value, componentId)
if (target) {
Object.assign(target, transformed)
}
} else {
const target = findComponentById(components.value, componentId)
if (target) {
target.composantModelId = previousModelId || null
target.composantModel = previousModel || null
}
toast.showError(result.error || 'Impossible de mettre à jour le modèle du composant')
}
} catch (error) {
console.error('Erreur lors de l\'assignation du modèle de composant:', error)
const target = findComponentById(components.value, componentId)
if (target) {
target.composantModelId = previousModelId || null
target.composantModel = previousModel || null
}
toast.showError('Impossible de mettre à jour le modèle du composant')
}
}
const submitSaveComponentModelModal = async () => {
if (!saveComponentAsModelModal.form.typeComposantId) {
toast.showError('Type de composant manquant pour créer le modèle')
return
}
if (!saveComponentAsModelModal.form.name.trim()) {
toast.showError('Veuillez renseigner un nom de modèle')
return
}
saveComponentAsModelModal.submitting = true
try {
const payload = {
name: saveComponentAsModelModal.form.name.trim(),
description: saveComponentAsModelModal.form.description.trim() || undefined,
typeComposantId: saveComponentAsModelModal.form.typeComposantId,
structure: saveComponentAsModelModal.structure,
}
const result = await createComponentModel(payload)
if (!result.success) {
toast.showError(result.error || 'Impossible de créer le modèle de composant')
return
}
const newModel = result.data
if (newModel?.typeComposantId) {
await loadComponentModels(newModel.typeComposantId)
}
const sourceComponent = saveComponentAsModelModal.component
if (sourceComponent && newModel?.id) {
await assignComponentModel({
componentId: sourceComponent.id,
composantModelId: newModel.id,
previousModelId: sourceComponent.composantModelId,
previousModel: sourceComponent.composantModel,
})
}
closeSaveComponentModelModal()
toast.showSuccess('Composant enregistré comme modèle')
} catch (error) {
console.error('Erreur lors de la création du modèle de composant:', error)
toast.showError('Impossible de créer le modèle de composant')
} finally {
saveComponentAsModelModal.submitting = false
}
}
const assignPieceModel = async ({ pieceId, pieceModelId, previousModelId, previousModel }) => {
if (!pieceId) return
try {
const result = await updatePieceApi(pieceId, {
pieceModelId: pieceModelId || null,
})
if (result.success) {
const transformed = transformCustomFields([result.data])[0]
const target = findPieceById(pieceId)
if (target) {
Object.assign(target, transformed)
}
} else {
const target = findPieceById(pieceId)
if (target) {
target.pieceModelId = previousModelId || null
target.pieceModel = previousModel || null
}
toast.showError(result.error || 'Impossible de mettre à jour le modèle de pièce')
}
} catch (error) {
console.error('Erreur lors de l\'assignation du modèle de pièce:', error)
const target = findPieceById(pieceId)
if (target) {
target.pieceModelId = previousModelId || null
target.pieceModel = previousModel || null
}
toast.showError('Impossible de mettre à jour le modèle de pièce')
}
}
// Méthodes pour les champs personnalisés de la machine
const setMachineCustomFieldValue = (fieldValueId, value) => {
const fieldValue = machine.value?.customFieldValues?.find(fv => fv.id === fieldValueId)

File diff suppressed because it is too large Load Diff

585
app/pages/models.vue Normal file
View File

@@ -0,0 +1,585 @@
<template>
<main class="container mx-auto px-6 py-8 space-y-8">
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div>
<h1 class="text-3xl font-bold text-gray-800">Catalogue de modèles</h1>
<p class="text-sm text-gray-500">
Administrez les modèles de composants et de pièces disponibles lors de la création des machines.
</p>
</div>
<div class="tabs tabs-boxed w-full md:w-auto">
<button
type="button"
class="tab"
:class="{ 'tab-active': activeTab === 'components' }"
@click="activeTab = 'components'"
>
Modèles de composants
</button>
<button
type="button"
class="tab"
:class="{ 'tab-active': activeTab === 'pieces' }"
@click="activeTab = 'pieces'"
>
Modèles de pièces
</button>
</div>
</div>
<!-- Component Models -->
<section v-if="activeTab === 'components'" class="space-y-4">
<div 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="selectedComponentType" 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>
<span class="text-xs text-gray-500">
{{ componentModelsList.length }} modèle(s)
</span>
</div>
<button type="button" class="btn btn-primary btn-sm w-full md:w-auto" @click="openComponentModal('create')">
<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-12">
<span class="loading loading-spinner loading-md"></span>
</div>
<div v-else-if="componentModelsList.length === 0" class="py-12 text-center text-sm text-gray-500">
Aucun modèle trouvé pour 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">Structure</th>
<th class="hidden lg:table-cell">Dernière modification</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="model in componentModelsList" :key="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 lg:table-cell">{{ formatStructurePreview(model.structure) }}</td>
<td class="hidden lg:table-cell">{{ formatDate(model.updatedAt || model.createdAt) }}</td>
<td class="text-right space-x-2">
<button type="button" class="btn btn-sm btn-outline" @click="openComponentModal('edit', model)">
Éditer
</button>
<button type="button" class="btn btn-sm btn-error" @click="confirmDeleteComponentModel(model)">
Supprimer
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</section>
<!-- Piece Models -->
<section v-else class="space-y-4">
<div 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="selectedPieceType" 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>
<span class="text-xs text-gray-500">
{{ pieceModelsList.length }} modèle(s)
</span>
</div>
<button type="button" class="btn btn-primary btn-sm w-full md:w-auto" @click="openPieceModal('create')">
<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-12">
<span class="loading loading-spinner loading-md"></span>
</div>
<div v-else-if="pieceModelsList.length === 0" class="py-12 text-center text-sm text-gray-500">
Aucun modèle trouvé pour 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">Dernière modification</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="model in pieceModelsList" :key="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">{{ formatDate(model.updatedAt || model.createdAt) }}</td>
<td class="text-right space-x-2">
<button type="button" class="btn btn-sm btn-outline" @click="openPieceModal('edit', model)">
Éditer
</button>
<button type="button" class="btn btn-sm btn-error" @click="confirmDeletePieceModel(model)">
Supprimer
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</section>
<!-- Component Model Modal -->
<div v-if="componentModal.open" class="modal modal-open">
<div class="modal-box max-w-4xl">
<h3 class="font-bold text-lg mb-1">
{{ componentModal.mode === 'create' ? 'Nouveau modèle de composant' : 'Modifier le modèle de composant' }}
</h3>
<p class="text-xs text-gray-500 mb-4">
Définissez le modèle de composant ainsi que sa structure par défaut (sous-composants, pièces et champs personnalisés).
</p>
<form class="space-y-5" @submit.prevent="submitComponentModal">
<div class="form-control">
<label class="label"><span class="label-text">Nom</span></label>
<input v-model="componentModal.form.name" type="text" class="input input-bordered input-sm" required />
</div>
<div class="form-control">
<label class="label"><span class="label-text">Description</span></label>
<textarea
v-model="componentModal.form.description"
class="textarea textarea-bordered textarea-sm"
rows="3"
placeholder="Notes sur ce modèle"
></textarea>
</div>
<div class="form-control">
<label class="label"><span class="label-text">Type de composant</span></label>
<select v-model="componentModal.form.typeComposantId" class="select select-bordered select-sm" required>
<option value="" disabled>Choisir 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="componentModal.form.structure" />
<div class="rounded-lg border border-base-200 bg-base-200/60 p-3">
<ModelStructureViewer :structure="componentModal.form.structure" />
</div>
<div class="modal-action">
<button type="button" class="btn btn-outline" @click="closeComponentModal">Annuler</button>
<button type="submit" class="btn btn-primary" :class="{ loading: componentModal.submitting }">
{{ componentModal.mode === 'create' ? 'Créer' : 'Enregistrer' }}
</button>
</div>
</form>
</div>
</div>
<!-- Piece Model Modal -->
<div v-if="pieceModal.open" class="modal modal-open">
<div class="modal-box max-w-md">
<h3 class="font-bold text-lg mb-1">
{{ pieceModal.mode === 'create' ? 'Nouveau modèle de pièce' : 'Modifier le modèle de pièce' }}
</h3>
<form class="space-y-4" @submit.prevent="submitPieceModal">
<div class="form-control">
<label class="label"><span class="label-text">Nom</span></label>
<input v-model="pieceModal.form.name" type="text" class="input input-bordered input-sm" required />
</div>
<div class="form-control">
<label class="label"><span class="label-text">Description</span></label>
<textarea
v-model="pieceModal.form.description"
class="textarea textarea-bordered textarea-sm"
rows="3"
placeholder="Notes sur ce modèle"
></textarea>
</div>
<div class="form-control">
<label class="label"><span class="label-text">Type de pièce</span></label>
<select v-model="pieceModal.form.typePieceId" class="select select-bordered select-sm" required>
<option value="" disabled>Choisir un type</option>
<option
v-for="type in pieceTypes"
:key="type.id"
:value="type.id"
>
{{ type.name }}
</option>
</select>
</div>
<div class="modal-action">
<button type="button" class="btn btn-outline" @click="closePieceModal">Annuler</button>
<button type="submit" class="btn btn-primary" :class="{ loading: pieceModal.submitting }">
{{ pieceModal.mode === 'create' ? 'Créer' : 'Enregistrer' }}
</button>
</div>
</form>
</div>
</div>
</main>
</template>
<script setup>
import { ref, computed, reactive, watch, onMounted } from 'vue'
import { useComponentModels } from '~/composables/useComponentModels'
import { usePieceModels } from '~/composables/usePieceModels'
import { useComponentTypes } from '~/composables/useComponentTypes'
import { usePieceTypes } from '~/composables/usePieceTypes'
import { useToast } from '~/composables/useToast'
import IconLucidePlus from '~icons/lucide/plus'
import IconLucideLayers from '~icons/lucide/layers'
import IconLucidePackage from '~icons/lucide/package'
import ComponentModelStructureEditor from '~/components/ComponentModelStructureEditor.vue'
import ModelStructureViewer from '~/components/ModelStructureViewer.vue'
import {
defaultStructure,
cloneStructure,
formatStructurePreview,
normalizeStructureForSave,
} from '~/shared/modelUtils'
const activeTab = ref('components')
const selectedComponentType = ref('all')
const selectedPieceType = ref('all')
const { componentTypes, loadComponentTypes } = useComponentTypes()
const { pieceTypes, loadPieceTypes } = usePieceTypes()
const {
componentModels,
loadComponentModels,
createComponentModel,
updateComponentModel,
deleteComponentModel,
loadingComponentModels,
getComponentModelsForType,
} = useComponentModels()
const {
pieceModels,
loadPieceModels,
createPieceModel,
updatePieceModel,
deletePieceModel,
loadingPieceModels,
getPieceModelsForType,
} = usePieceModels()
const toast = useToast()
const componentModal = reactive({
open: false,
mode: 'create',
submitting: false,
previousTypeId: null,
form: {
id: null,
name: '',
description: '',
typeComposantId: '',
structure: defaultStructure(),
},
})
const pieceModal = reactive({
open: false,
mode: 'create',
submitting: false,
previousTypeId: null,
form: {
id: null,
name: '',
description: '',
typePieceId: '',
},
})
const componentModelsList = computed(() => {
if (selectedComponentType.value === 'all') {
return componentModels.value
}
return getComponentModelsForType(selectedComponentType.value) || []
})
const pieceModelsList = computed(() => {
if (selectedPieceType.value === 'all') {
return pieceModels.value
}
return getPieceModelsForType(selectedPieceType.value) || []
})
const formatDate = (value) => {
if (!value) return '—'
const date = new Date(value)
return date.toLocaleDateString('fr-FR', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
})
}
const ensureTypeSelected = (typeId, list) => {
if (typeId && list.some((type) => type.id === typeId)) {
return typeId
}
return list[0]?.id || ''
}
const openComponentModal = (mode, model) => {
componentModal.mode = mode
componentModal.open = true
componentModal.submitting = false
componentModal.previousTypeId = model?.typeComposantId || null
if (mode === 'edit' && model) {
componentModal.form = {
id: model.id,
name: model.name,
description: model.description || '',
typeComposantId: model.typeComposantId || model.typeComposant?.id || '',
structure: cloneStructure(model.structure || defaultStructure()),
}
} else {
componentModal.form = {
id: null,
name: '',
description: '',
typeComposantId: ensureTypeSelected(selectedComponentType.value !== 'all' ? selectedComponentType.value : '', componentTypes.value),
structure: defaultStructure(),
}
}
}
const closeComponentModal = () => {
componentModal.open = false
}
const submitComponentModal = async () => {
if (!componentModal.form.typeComposantId) {
toast.showError('Veuillez sélectionner un type de composant')
return
}
componentModal.submitting = true
try {
if (componentModal.mode === 'create') {
await createComponentModel({
name: componentModal.form.name.trim(),
description: componentModal.form.description.trim() || undefined,
typeComposantId: componentModal.form.typeComposantId,
structure: normalizeStructureForSave(componentModal.form.structure),
})
} else {
await updateComponentModel(componentModal.form.id, {
name: componentModal.form.name.trim(),
description: componentModal.form.description.trim() || undefined,
typeComposantId: componentModal.form.typeComposantId,
structure: normalizeStructureForSave(componentModal.form.structure),
})
}
await refreshComponentModels(componentModal.form.typeComposantId)
if (selectedComponentType.value === 'all') {
await refreshComponentModels()
}
if (
componentModal.mode === 'edit' &&
componentModal.previousTypeId &&
componentModal.previousTypeId !== componentModal.form.typeComposantId
) {
await refreshComponentModels(componentModal.previousTypeId)
}
closeComponentModal()
} catch (error) {
toast.showError('Impossible d\'enregistrer le modèle de composant')
} finally {
componentModal.submitting = false
}
}
const confirmDeleteComponentModel = async (model) => {
if (!confirm(`Supprimer le modèle "${model.name}" ?`)) return
try {
const result = await deleteComponentModel(model.id)
if (result.success) {
await refreshComponentModels(model.typeComposantId)
if (selectedComponentType.value === 'all') {
await refreshComponentModels()
}
}
} catch (error) {
toast.showError('Impossible de supprimer ce modèle')
}
}
const openPieceModal = (mode, model) => {
pieceModal.mode = mode
pieceModal.open = true
pieceModal.submitting = false
pieceModal.previousTypeId = model?.typePieceId || null
if (mode === 'edit' && model) {
pieceModal.form = {
id: model.id,
name: model.name,
description: model.description || '',
typePieceId: model.typePieceId || model.typePiece?.id || '',
}
} else {
pieceModal.form = {
id: null,
name: '',
description: '',
typePieceId: ensureTypeSelected(selectedPieceType.value !== 'all' ? selectedPieceType.value : '', pieceTypes.value),
}
}
}
const closePieceModal = () => {
pieceModal.open = false
}
const submitPieceModal = async () => {
if (!pieceModal.form.typePieceId) {
toast.showError('Veuillez sélectionner un type de pièce')
return
}
pieceModal.submitting = true
try {
if (pieceModal.mode === 'create') {
await createPieceModel({
name: pieceModal.form.name.trim(),
description: pieceModal.form.description.trim() || undefined,
typePieceId: pieceModal.form.typePieceId,
structure: {},
})
} else {
await updatePieceModel(pieceModal.form.id, {
name: pieceModal.form.name.trim(),
description: pieceModal.form.description.trim() || undefined,
typePieceId: pieceModal.form.typePieceId,
})
}
await refreshPieceModels(pieceModal.form.typePieceId)
if (selectedPieceType.value === 'all') {
await refreshPieceModels()
}
if (
pieceModal.mode === 'edit' &&
pieceModal.previousTypeId &&
pieceModal.previousTypeId !== pieceModal.form.typePieceId
) {
await refreshPieceModels(pieceModal.previousTypeId)
}
closePieceModal()
} catch (error) {
toast.showError('Impossible d\'enregistrer le modèle de pièce')
} finally {
pieceModal.submitting = false
}
}
const confirmDeletePieceModel = async (model) => {
if (!confirm(`Supprimer le modèle "${model.name}" ?`)) return
try {
const result = await deletePieceModel(model.id)
if (result.success) {
await refreshPieceModels(model.typePieceId)
if (selectedPieceType.value === 'all') {
await refreshPieceModels()
}
}
} catch (error) {
toast.showError('Impossible de supprimer ce modèle')
}
}
const refreshComponentModels = async (typeId) => {
if (typeId) {
await loadComponentModels(typeId)
} else {
await loadComponentModels()
}
}
const refreshPieceModels = async (typeId) => {
if (typeId) {
await loadPieceModels(typeId)
} else {
await loadPieceModels()
}
}
watch(selectedComponentType, async (value) => {
await refreshComponentModels(value === 'all' ? undefined : value)
}, { immediate: true })
watch(selectedPieceType, async (value) => {
await refreshPieceModels(value === 'all' ? undefined : value)
}, { immediate: true })
onMounted(async () => {
await Promise.all([
loadComponentTypes(),
loadPieceTypes(),
])
})
</script>

View File

@@ -29,49 +29,63 @@
<!-- 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"
>
<IconLucideMinus
v-if="globalExpandState.expanded"
class="w-4 h-4 mr-2"
aria-hidden="true"
/>
<IconLucidePlus
v-else
class="w-4 h-4 mr-2"
aria-hidden="true"
/>
{{ 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>
<div class="space-y-4">
<TypeComponentDisplay
v-for="(component, componentIndex) in type.components"
:key="componentIndex"
:component="component"
:global-expand-state="globalExpandState"
/>
<!-- Familles de composants -->
<div v-if="componentRequirementCount > 0" class="mb-8 space-y-3">
<h3 class="text-lg font-semibold">Familles de composants</h3>
<div class="space-y-3">
<div
v-for="requirement in type.componentRequirements"
:key="requirement.id"
class="border border-base-200 rounded-lg p-4 bg-base-100"
>
<div class="flex items-start justify-between gap-2">
<div>
<h4 class="text-sm font-semibold">
{{ requirement.label || requirement.typeComposant?.name || 'Famille' }}
</h4>
<p class="text-xs text-gray-500">
Type : {{ requirement.typeComposant?.name || 'Non défini' }}
</p>
</div>
<span class="badge badge-outline badge-sm">
Min {{ toDisplayCount(requirement.minCount, requirement.required ? 1 : 0) }}
Max {{ toDisplayCount(requirement.maxCount, '∞') }}
</span>
</div>
<p class="text-xs text-gray-500 mt-2">
{{ requirement.allowNewModels ? 'Création de modèles autorisée' : 'Modèles existants uniquement' }}
</p>
</div>
</div>
</div>
<!-- Affichage des pièces principales existantes -->
<div v-if="type.machinePieces && type.machinePieces.length > 0" class="mb-8">
<h3 class="text-lg font-semibold mb-4">Pièces principales existantes</h3>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<TypeMachinePieceDisplay
v-for="(piece, pieceIndex) in type.machinePieces"
:key="pieceIndex"
:piece="piece"
:global-expand-state="globalExpandState"
/>
<!-- Groupes de pièces -->
<div v-if="pieceRequirementCount > 0" class="mb-8 space-y-3">
<h3 class="text-lg font-semibold">Groupes de pièces</h3>
<div class="space-y-3">
<div
v-for="requirement in type.pieceRequirements"
:key="requirement.id"
class="border border-base-200 rounded-lg p-4 bg-base-100"
>
<div class="flex items-start justify-between gap-2">
<div>
<h4 class="text-sm font-semibold">
{{ requirement.label || requirement.typePiece?.name || 'Groupe' }}
</h4>
<p class="text-xs text-gray-500">
Type : {{ requirement.typePiece?.name || 'Non défini' }}
</p>
</div>
<span class="badge badge-outline badge-sm">
Min {{ toDisplayCount(requirement.minCount, requirement.required ? 1 : 0) }}
Max {{ toDisplayCount(requirement.maxCount, '∞') }}
</span>
</div>
<p class="text-xs text-gray-500 mt-2">
{{ requirement.allowNewModels ? 'Création de modèles autorisée' : 'Modèles existants uniquement' }}
</p>
</div>
</div>
</div>
</div>
@@ -99,8 +113,6 @@ import { useRoute } from 'vue-router'
import { useMachineTypesApi } from '~/composables/useMachineTypesApi'
import { useToast } from '~/composables/useToast'
import IconLucideSquarePen from '~icons/lucide/square-pen'
import IconLucideMinus from '~icons/lucide/minus'
import IconLucidePlus from '~icons/lucide/plus'
const route = useRoute()
const { getMachineTypeById } = useMachineTypesApi()
@@ -109,20 +121,14 @@ const { showError } = useToast()
const type = ref(null)
const loading = ref(true)
const globalExpandState = reactive({
expanded: true,
id: 0
})
const componentRequirementCount = computed(() => type.value?.componentRequirements?.length || 0)
const pieceRequirementCount = computed(() => type.value?.pieceRequirements?.length || 0)
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 toDisplayCount = (value, fallback) => {
if (value === null || value === undefined) {
return fallback
}
return value
}
onMounted(async () => {

View File

@@ -67,10 +67,69 @@ const editedType = ref({
category: '',
maintenanceFrequency: '',
customFields: [],
machinePieces: [],
components: []
componentRequirements: [],
pieceRequirements: [],
})
const parseOptions = (field = {}) => {
if (field.type !== 'select') return []
if (field.optionsText && typeof field.optionsText === 'string') {
return field.optionsText
.split('\n')
.map(option => option.trim())
.filter(Boolean)
}
if (Array.isArray(field.options)) {
return field.options
.map(option => String(option).trim())
.filter(Boolean)
}
return []
}
const normalizeCustomFields = (fields = []) =>
fields
.filter(field => field?.name && field.name.trim() !== '')
.map(field => ({
name: field.name,
type: field.type || '',
required: !!field.required,
defaultValue: field.defaultValue || '',
options: parseOptions(field)
}))
const toIntegerOrNull = (value, fallback = null) => {
if (value === '' || value === undefined || value === null) {
return fallback
}
const parsed = Number(value)
return Number.isFinite(parsed) ? parsed : fallback
}
const normalizeComponentRequirements = (requirements = []) =>
requirements
.filter(req => req?.typeComposantId)
.map(req => ({
typeComposantId: req.typeComposantId,
label: req.label?.trim() ? req.label.trim() : undefined,
minCount: toIntegerOrNull(req.minCount, 1),
maxCount: toIntegerOrNull(req.maxCount, null),
required: req.required ?? true,
allowNewModels: req.allowNewModels ?? true,
}))
const normalizePieceRequirements = (requirements = []) =>
requirements
.filter(req => req?.typePieceId)
.map(req => ({
typePieceId: req.typePieceId,
label: req.label?.trim() ? req.label.trim() : undefined,
minCount: toIntegerOrNull(req.minCount, 0),
maxCount: toIntegerOrNull(req.maxCount, null),
required: req.required ?? false,
allowNewModels: req.allowNewModels ?? true,
}))
const saveChanges = async () => {
try {
saving.value = true
@@ -80,80 +139,9 @@ const saveChanges = async () => {
// Préparer les données pour l'API
const updatedType = {
...currentEditedType,
// Traiter les champs personnalisés
customFields: (currentEditedType.customFields || [])
.filter(field => field.name.trim() !== '')
.map(field => ({
name: field.name,
type: field.type,
required: field.required || false,
defaultValue: field.defaultValue || '',
options: field.type === 'select' && field.optionsText
? field.optionsText.split('\n').filter(opt => opt.trim() !== '')
: []
})),
// Traiter les pièces principales
machinePieces: (currentEditedType.machinePieces || [])
.filter(piece => piece.name.trim() !== '')
.map(piece => ({
name: piece.name,
reference: piece.reference || '',
constructeur: piece.constructeur || '',
emplacement: piece.emplacement || '',
prix: piece.prix || null,
customFields: (piece.customFields || [])
.filter(field => field.name.trim() !== '')
.map(field => ({
name: field.name,
type: field.type,
required: field.required || false,
defaultValue: field.defaultValue || '',
options: field.type === 'select' && field.optionsText
? field.optionsText.split('\n').filter(opt => opt.trim() !== '')
: []
}))
})),
// Traiter les composants
components: (currentEditedType.components || [])
.filter(comp => comp.name.trim() !== '')
.map(comp => ({
name: comp.name,
reference: comp.reference || '',
constructeur: comp.constructeur || '',
emplacement: comp.emplacement || '',
prix: comp.prix || null,
customFields: (comp.customFields || [])
.filter(field => field.name.trim() !== '')
.map(field => ({
name: field.name,
type: field.type,
required: field.required || false,
defaultValue: field.defaultValue || '',
options: field.type === 'select' && field.optionsText
? field.optionsText.split('\n').filter(opt => opt.trim() !== '')
: []
})),
pieces: (comp.pieces || [])
.filter(piece => piece.name.trim() !== '')
.map(piece => ({
name: piece.name,
reference: piece.reference || '',
constructeur: piece.constructeur || '',
emplacement: piece.emplacement || '',
prix: piece.prix || null,
customFields: (piece.customFields || [])
.filter(field => field.name.trim() !== '')
.map(field => ({
name: field.name,
type: field.type,
required: field.required || false,
defaultValue: field.defaultValue || '',
options: field.type === 'select' && field.optionsText
? field.optionsText.split('\n').filter(opt => opt.trim() !== '')
: []
}))
}))
}))
customFields: normalizeCustomFields(currentEditedType.customFields),
componentRequirements: normalizeComponentRequirements(currentEditedType.componentRequirements),
pieceRequirements: normalizePieceRequirements(currentEditedType.pieceRequirements),
}
const result = await updateMachineType(type.value.id, updatedType)
@@ -193,8 +181,8 @@ onMounted(async () => {
category: type.value.category || '',
maintenanceFrequency: type.value.maintenanceFrequency || '',
customFields: type.value.customFields || [],
machinePieces: type.value.machinePieces || [],
components: type.value.components || []
componentRequirements: type.value.componentRequirements || [],
pieceRequirements: type.value.pieceRequirements || [],
}
} else {
console.error('Failed to load type:', result.error)

View File

@@ -42,13 +42,14 @@
<div class="badge badge-primary">{{ type.category }}</div>
</div>
<p class="text-gray-600 mb-4">{{ type.description }}</p>
<div class="space-y-2">
<div class="flex items-center text-sm text-gray-500">
<IconLucidePackage
class="w-4 h-4 mr-2"
aria-hidden="true"
/>
{{ type.machinePieces?.length || 0 }} pièces totales
<div class="space-y-2 text-sm text-gray-500">
<div class="flex items-center gap-2">
<IconLucidePackage class="w-4 h-4" aria-hidden="true" />
<span>{{ type.componentRequirements?.length || 0 }} famille(s) de composants</span>
</div>
<div class="flex items-center gap-2">
<IconLucideLayoutGrid class="w-4 h-4" aria-hidden="true" />
<span>{{ type.pieceRequirements?.length || 0 }} groupe(s) de pièces</span>
</div>
</div>
<div class="card-actions justify-end mt-4">