set up new view for skeleton hiearchi
This commit is contained in:
@@ -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 = () => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
585
app/pages/models.vue
Normal 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>
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user