From 936a9d74cadc5414a238c5f9f308d5dff99063be Mon Sep 17 00:00:00 2001 From: Matthieu Date: Mon, 22 Sep 2025 08:34:05 +0200 Subject: [PATCH] set up new view for skeleton hiearchi --- app/app.vue | 18 + app/components/ComponentHierarchy.vue | 19 +- app/components/ComponentItem.vue | 133 +- .../ComponentModelStructureEditor.vue | 242 +++ app/components/ModelStructureViewer.vue | 36 + app/components/PieceItem.vue | 82 +- .../StructureSubComponentEditor.vue | 260 ++++ app/components/TypeComponentDisplay.vue | 474 ------ .../TypeEditComponentRequirementsSection.vue | 191 +++ app/components/TypeEditComponentsSection.vue | 282 ---- app/components/TypeEditForm.vue | 55 +- .../TypeEditMachinePiecesSection.vue | 75 - .../TypeEditPieceRequirementsSection.vue | 191 +++ app/components/TypeInfoDisplay.vue | 6 +- app/components/TypeMachinePieceDisplay.vue | 99 -- app/components/TypeMachinePieceForm.vue | 404 ----- app/composables/useComponentModels.js | 131 ++ app/composables/useComponentTypes.js | 95 ++ app/composables/useMachineTypes.js | 654 -------- app/composables/usePieceModels.js | 131 ++ app/composables/usePieceTypes.js | 95 ++ app/pages/generator.vue | 66 +- app/pages/index.vue | 8 +- app/pages/machine/[id].vue | 558 ++++++- app/pages/machines.vue | 1364 ++++++++++++++++- app/pages/models.vue | 585 +++++++ app/pages/type/[id].vue | 116 +- app/pages/type/edit/[id].vue | 144 +- app/pages/types.vue | 15 +- app/shared/modelUtils.ts | 289 ++++ 30 files changed, 4530 insertions(+), 2288 deletions(-) create mode 100644 app/components/ComponentModelStructureEditor.vue create mode 100644 app/components/ModelStructureViewer.vue create mode 100644 app/components/StructureSubComponentEditor.vue delete mode 100644 app/components/TypeComponentDisplay.vue create mode 100644 app/components/TypeEditComponentRequirementsSection.vue delete mode 100644 app/components/TypeEditComponentsSection.vue delete mode 100644 app/components/TypeEditMachinePiecesSection.vue create mode 100644 app/components/TypeEditPieceRequirementsSection.vue delete mode 100644 app/components/TypeMachinePieceDisplay.vue delete mode 100644 app/components/TypeMachinePieceForm.vue create mode 100644 app/composables/useComponentModels.js create mode 100644 app/composables/useComponentTypes.js delete mode 100644 app/composables/useMachineTypes.js create mode 100644 app/composables/usePieceModels.js create mode 100644 app/composables/usePieceTypes.js create mode 100644 app/pages/models.vue create mode 100644 app/shared/modelUtils.ts diff --git a/app/app.vue b/app/app.vue index 84268b2..9c4b0e0 100644 --- a/app/app.vue +++ b/app/app.vue @@ -44,6 +44,15 @@ Types de Machines +
  • + + Modèles + +
  • +
  • + + Modèles + +
  • @@ -33,8 +40,16 @@ defineProps({ toggleToken: { type: Number, default: 0 - } + }, + componentModelOptionsProvider: { + type: Function, + default: () => [], + }, + pieceModelOptionsProvider: { + type: Function, + default: () => [], + }, }) -defineEmits(['update', 'edit-piece']) +defineEmits(['update', 'edit-piece', 'assign-model', 'assign-piece-model', 'custom-field-update', 'create-model-from-component']) diff --git a/app/components/ComponentItem.vue b/app/components/ComponentItem.vue index 6cfc1d7..18e3113 100644 --- a/app/components/ComponentItem.vue +++ b/app/components/ComponentItem.vue @@ -27,6 +27,18 @@ {{ component.constructeur?.name }} {{ component.emplacement }} {{ component.prix }}€ + + Groupe : {{ component.typeMachineComponentRequirement.label || component.typeMachineComponentRequirement.typeComposant?.name || 'Non défini' }} + + + Modèle : {{ component.composantModel.name }} + @@ -99,6 +111,44 @@ + +
    +
    + +
    + + +
    +
    +
    @@ -243,9 +293,11 @@ :key="piece.id" :piece="piece" :is-edit-mode="isEditMode" + :piece-model-options="pieceModelOptionsProvider(piece)" @update="updatePiece" @edit="editPiece" @custom-field-update="updatePieceCustomField" + @assign-model="emitAssignPieceModel" /> @@ -261,8 +313,13 @@ :is-edit-mode="isEditMode" :collapse-all="collapseAll" :toggle-token="toggleToken" + :component-model-options="componentModelOptionsProvider(subComponent)" + :component-model-options-provider="componentModelOptionsProvider" + :piece-model-options-provider="pieceModelOptionsProvider" @update="$emit('update', $event)" @edit-piece="$emit('edit-piece', $event)" + @assign-model="$emit('assign-model', $event)" + @assign-piece-model="$emit('assign-piece-model', $event)" /> @@ -284,23 +341,42 @@ import IconLucideChevronRight from '~icons/lucide/chevron-right' const props = defineProps({ component: { type: Object, - required: true + required: true, }, isEditMode: { type: Boolean, - default: false + default: false, }, collapseAll: { type: Boolean, - default: true + default: true, }, toggleToken: { type: Number, - default: 0 - } + default: 0, + }, + componentModelOptions: { + type: Array, + default: () => [], + }, + componentModelOptionsProvider: { + type: Function, + default: () => [], + }, + pieceModelOptionsProvider: { + type: Function, + default: () => [], + }, }) -const emit = defineEmits(['update', 'edit-piece']) +const emit = defineEmits([ + 'update', + 'edit-piece', + 'custom-field-update', + 'assign-model', + 'assign-piece-model', + 'create-model-from-component', +]) const isCollapsed = ref(true) const selectedFiles = ref([]) @@ -312,6 +388,13 @@ const documentIcon = (doc) => getFileIcon({ name: doc.filename || doc.name, mime const previewDocument = ref(null) const previewVisible = ref(false) +const selectedComponentModelId = computed(() => props.component.composantModelId || props.component.composantModel?.id || '') +const componentModelOptionsList = computed(() => { + const provided = props.componentModelOptionsProvider(props.component) + return Array.isArray(provided) && provided.length ? provided : props.componentModelOptions +}) +const pieceModelOptionsList = computed(() => props.pieceModelOptionsProvider(props.component) || []) + const handleConstructeurChange = async (value) => { props.component.constructeurId = value await updateComponent() @@ -327,15 +410,14 @@ watch( ensureDocumentsLoaded() } }, - { immediate: true } + { immediate: true }, ) - watch( () => props.component.documents, (docs) => { documentsLoaded.value = !!(docs && docs.length) - } + }, ) const toggleCollapse = () => { @@ -345,13 +427,11 @@ const toggleCollapse = () => { } } -// Methods const updateComponent = () => { emit('update', props.component) } -const updateComponentCustomField = (field) => { - // Mettre à jour le champ personnalisé du composant +const updateComponentCustomField = () => { emit('update', props.component) } @@ -364,10 +444,29 @@ const editPiece = (piece) => { } const updatePieceCustomField = (fieldUpdate) => { - // Forward to parent + emit('custom-field-update', fieldUpdate) emit('edit-piece', { ...fieldUpdate, type: 'custom-field-update' }) } +const assignComponentModel = (value) => { + const previousModelId = props.component.composantModelId || props.component.composantModel?.id || null + const previousModel = props.component.composantModel || null + props.component.composantModelId = value || null + if (!value) { + props.component.composantModel = null + } + emit('assign-model', { + componentId: props.component.id, + composantModelId: value || null, + previousModelId, + previousModel, + }) +} + +const emitAssignPieceModel = (payload) => { + emit('assign-piece-model', payload) +} + const ensureDocumentsLoaded = async () => { if (documentsLoaded.value || !props.component?.id) return await refreshDocuments() @@ -393,9 +492,9 @@ const handleFilesAdded = async (files) => { const result = await uploadDocuments( { files, - context: { composantId: props.component.id } + context: { composantId: props.component.id }, }, - { updateStore: false } + { updateStore: false }, ) if (result.success) { @@ -413,7 +512,7 @@ const removeDocument = async (documentId) => { if (!documentId) return const result = await deleteDocument(documentId, { updateStore: false }) if (result.success) { - props.component.documents = (props.component.documents || []).filter(doc => doc.id !== documentId) + props.component.documents = (props.component.documents || []).filter((doc) => doc.id !== documentId) } } @@ -450,4 +549,4 @@ const formatSize = (size) => { const formatted = size / Math.pow(1024, index) return `${formatted.toFixed(1)} ${units[index]}` } - + diff --git a/app/components/ComponentModelStructureEditor.vue b/app/components/ComponentModelStructureEditor.vue new file mode 100644 index 0000000..6ff316a --- /dev/null +++ b/app/components/ComponentModelStructureEditor.vue @@ -0,0 +1,242 @@ + + + diff --git a/app/components/ModelStructureViewer.vue b/app/components/ModelStructureViewer.vue new file mode 100644 index 0000000..cc18ed1 --- /dev/null +++ b/app/components/ModelStructureViewer.vue @@ -0,0 +1,36 @@ + + + diff --git a/app/components/PieceItem.vue b/app/components/PieceItem.vue index 6d37fd4..8140cfd 100644 --- a/app/components/PieceItem.vue +++ b/app/components/PieceItem.vue @@ -24,6 +24,23 @@ {{ pieceData.name }} +
    + + Groupe : {{ piece.typeMachinePieceRequirement.label || piece.typeMachinePieceRequirement.typePiece?.name || 'Non défini' }} + + + Modèle : {{ piece.pieceModel.name }} + + + Rattachée à {{ piece.parentComponentName }} + +
    @@ -81,6 +98,32 @@
    +
    + + +
    +
    Champs personnalisés
    @@ -260,15 +303,19 @@ import IconLucidePackage from '~icons/lucide/package' const props = defineProps({ piece: { type: Object, - required: true + required: true, }, isEditMode: { type: Boolean, - default: false - } + default: false, + }, + pieceModelOptions: { + type: Array, + default: () => [], + }, }) -const emit = defineEmits(['update', 'edit', 'custom-field-update']) +const emit = defineEmits(['update', 'edit', 'custom-field-update', 'assign-model']) // Données locales isolées pour cette pièce const pieceData = reactive({ @@ -286,6 +333,8 @@ const pieceDocuments = computed(() => props.piece.documents || []) const documentIcon = (doc) => getFileIcon({ name: doc.filename || doc.name, mime: doc.mimeType }) const previewDocument = ref(null) const previewVisible = ref(false) +const selectedPieceModelId = computed(() => props.piece.pieceModelId || props.piece.pieceModel?.id || '') +const pieceModelOptions = computed(() => props.pieceModelOptions || []) const handleConstructeurChange = (value) => { props.piece.constructeurId = value @@ -399,6 +448,21 @@ const updatePiece = () => { }) } +const assignPieceModel = (value) => { + const previousModelId = props.piece.pieceModelId || props.piece.pieceModel?.id || null + const previousModel = props.piece.pieceModel || null + props.piece.pieceModelId = value || null + if (!value) { + props.piece.pieceModel = null + } + emit('assign-model', { + pieceId: props.piece.id, + pieceModelId: value || null, + previousModelId, + previousModel, + }) +} + const updateCustomFieldValue = async (fieldValueId) => { const fieldValue = props.piece.customFieldValues?.find(fv => fv.id === fieldValueId) if (fieldValue) { @@ -419,6 +483,16 @@ watch(() => props.piece.customFieldValues, () => { console.log('PieceItem - customFieldValues updated:', props.piece.customFieldValues) }, { deep: true }) +watch( + () => [props.piece.name, props.piece.reference, props.piece.emplacement, props.piece.prix], + () => { + pieceData.name = props.piece.name || '' + pieceData.reference = props.piece.reference || '' + pieceData.emplacement = props.piece.emplacement || '' + pieceData.prix = props.piece.prix || '' + }, +) + onMounted(() => { // Initialiser les données avec les props pieceData.name = props.piece.name || '' diff --git a/app/components/StructureSubComponentEditor.vue b/app/components/StructureSubComponentEditor.vue new file mode 100644 index 0000000..637cd08 --- /dev/null +++ b/app/components/StructureSubComponentEditor.vue @@ -0,0 +1,260 @@ + + + diff --git a/app/components/TypeComponentDisplay.vue b/app/components/TypeComponentDisplay.vue deleted file mode 100644 index 1083064..0000000 --- a/app/components/TypeComponentDisplay.vue +++ /dev/null @@ -1,474 +0,0 @@ - - - diff --git a/app/components/TypeEditComponentRequirementsSection.vue b/app/components/TypeEditComponentRequirementsSection.vue new file mode 100644 index 0000000..875e1f5 --- /dev/null +++ b/app/components/TypeEditComponentRequirementsSection.vue @@ -0,0 +1,191 @@ + + + diff --git a/app/components/TypeEditComponentsSection.vue b/app/components/TypeEditComponentsSection.vue deleted file mode 100644 index 51fa58d..0000000 --- a/app/components/TypeEditComponentsSection.vue +++ /dev/null @@ -1,282 +0,0 @@ - - - diff --git a/app/components/TypeEditForm.vue b/app/components/TypeEditForm.vue index f6d2bcc..ced6259 100644 --- a/app/components/TypeEditForm.vue +++ b/app/components/TypeEditForm.vue @@ -16,18 +16,14 @@ @update:model-value="(value) => (formData.customFields = value)" /> - - @@ -38,10 +34,10 @@ import { reactive, ref, watch } from 'vue' import TypeEditActionsBar from '~/components/TypeEditActionsBar.vue' import TypeEditBaseInfoSection from '~/components/TypeEditBaseInfoSection.vue' -import TypeEditComponentsSection from '~/components/TypeEditComponentsSection.vue' import TypeEditCustomFieldsSection from '~/components/TypeEditCustomFieldsSection.vue' -import TypeEditMachinePiecesSection from '~/components/TypeEditMachinePiecesSection.vue' import TypeEditToolbar from '~/components/TypeEditToolbar.vue' +import TypeEditComponentRequirementsSection from '~/components/TypeEditComponentRequirementsSection.vue' +import TypeEditPieceRequirementsSection from '~/components/TypeEditPieceRequirementsSection.vue' const props = defineProps({ modelValue: { @@ -58,45 +54,14 @@ const emit = defineEmits(['update:modelValue', 'submit']) const deepClone = (value) => JSON.parse(JSON.stringify(value)) -const normalizePieces = (pieces = []) => { - const cloned = deepClone(pieces) - return cloned.map((piece) => ({ - ...piece, - constructeur: typeof piece.constructeur === 'string' - ? { id: null, name: piece.constructeur } - : piece.constructeur || null, - constructeurId: - piece.constructeurId || - (typeof piece.constructeur === 'object' && piece.constructeur?.id) || - null, - customFields: piece.customFields || [], - })) -} - -const normalizeComponents = (components = []) => { - const cloned = deepClone(components) - return cloned.map((component) => ({ - ...component, - constructeur: typeof component.constructeur === 'string' - ? { id: null, name: component.constructeur } - : component.constructeur || null, - constructeurId: - component.constructeurId || - (typeof component.constructeur === 'object' && component.constructeur?.id) || - null, - customFields: component.customFields || [], - pieces: normalizePieces(component.pieces || []), - })) -} - const createDefaultForm = (source = {}) => ({ name: source.name || '', description: source.description || '', category: source.category || '', maintenanceFrequency: source.maintenanceFrequency || '', customFields: deepClone(source.customFields || []), - machinePieces: normalizePieces(source.machinePieces || []), - components: normalizeComponents(source.components || []), + componentRequirements: deepClone(source.componentRequirements || []), + pieceRequirements: deepClone(source.pieceRequirements || []), }) const formData = reactive(createDefaultForm(props.modelValue)) diff --git a/app/components/TypeEditMachinePiecesSection.vue b/app/components/TypeEditMachinePiecesSection.vue deleted file mode 100644 index b97dea2..0000000 --- a/app/components/TypeEditMachinePiecesSection.vue +++ /dev/null @@ -1,75 +0,0 @@ - - - diff --git a/app/components/TypeEditPieceRequirementsSection.vue b/app/components/TypeEditPieceRequirementsSection.vue new file mode 100644 index 0000000..1424ad1 --- /dev/null +++ b/app/components/TypeEditPieceRequirementsSection.vue @@ -0,0 +1,191 @@ + + + diff --git a/app/components/TypeInfoDisplay.vue b/app/components/TypeInfoDisplay.vue index 7ee594b..cf08977 100644 --- a/app/components/TypeInfoDisplay.vue +++ b/app/components/TypeInfoDisplay.vue @@ -5,8 +5,8 @@

    Catégorie: {{ type.category || 'Non définie' }}

    Maintenance: {{ type.maintenanceFrequency || 'Non définie' }}

    -

    Composants: {{ type.components?.length || 0 }}

    -

    Pièces principales: {{ type.machinePieces?.length || 0 }}

    +

    Familles de composants: {{ type.componentRequirements?.length || 0 }}

    +

    Groupes de pièces: {{ type.pieceRequirements?.length || 0 }}

    Description: {{ type.description }}

    @@ -20,4 +20,4 @@ defineProps({ required: true } }) - \ No newline at end of file + diff --git a/app/components/TypeMachinePieceDisplay.vue b/app/components/TypeMachinePieceDisplay.vue deleted file mode 100644 index 79c7103..0000000 --- a/app/components/TypeMachinePieceDisplay.vue +++ /dev/null @@ -1,99 +0,0 @@ - - - diff --git a/app/components/TypeMachinePieceForm.vue b/app/components/TypeMachinePieceForm.vue deleted file mode 100644 index d1868c3..0000000 --- a/app/components/TypeMachinePieceForm.vue +++ /dev/null @@ -1,404 +0,0 @@ - - - diff --git a/app/composables/useComponentModels.js b/app/composables/useComponentModels.js new file mode 100644 index 0000000..8a79f3c --- /dev/null +++ b/app/composables/useComponentModels.js @@ -0,0 +1,131 @@ +import { ref, computed } from 'vue' +import { useApi } from './useApi' +import { useToast } from './useToast' + +const componentModelsBuckets = ref({}) +const loadingComponentModels = ref(false) + +export function useComponentModels() { + const { get, post, patch, delete: del } = useApi() + const { showSuccess, showError } = useToast() + + const loadComponentModels = async (typeComposantId) => { + loadingComponentModels.value = true + try { + const query = typeComposantId ? `?typeComposantId=${encodeURIComponent(typeComposantId)}` : '' + const result = await get(`/types/composants/models${query}`) + if (result.success) { + const key = typeComposantId || '__all__' + componentModelsBuckets.value = { + ...componentModelsBuckets.value, + [key]: result.data, + } + } + return result + } catch (error) { + showError(`Impossible de charger les modèles de composant: ${error.message}`) + return { success: false, error: error.message } + } finally { + loadingComponentModels.value = false + } + } + + const createComponentModel = async (payload) => { + loadingComponentModels.value = true + try { + const result = await post('/types/composants/models', payload) + if (result.success) { + const key = result.data?.typeComposantId || '__all__' + const bucket = componentModelsBuckets.value[key] || [] + componentModelsBuckets.value = { + ...componentModelsBuckets.value, + [key]: [...bucket, result.data], + } + showSuccess(`Modèle de composant "${result.data.name}" créé`) + } + return result + } catch (error) { + showError(`Erreur lors de la création du modèle de composant: ${error.message}`) + return { success: false, error: error.message } + } finally { + loadingComponentModels.value = false + } + } + + const updateComponentModel = async (id, payload) => { + loadingComponentModels.value = true + try { + const result = await patch(`/types/composants/models/${id}`, payload) + if (result.success) { + const key = result.data?.typeComposantId || '__all__' + const bucket = componentModelsBuckets.value[key] || [] + const updatedBucket = bucket.map((model) => + model.id === id ? result.data : model + ) + componentModelsBuckets.value = { + ...componentModelsBuckets.value, + [key]: updatedBucket, + } + showSuccess(`Modèle de composant "${result.data.name}" mis à jour`) + } + return result + } catch (error) { + showError(`Erreur lors de la mise à jour du modèle de composant: ${error.message}`) + return { success: false, error: error.message } + } finally { + loadingComponentModels.value = false + } + } + + const deleteComponentModel = async (id) => { + loadingComponentModels.value = true + try { + const result = await del(`/types/composants/models/${id}`) + if (result.success) { + const updatedBuckets = {} + for (const [key, bucket] of Object.entries(componentModelsBuckets.value)) { + updatedBuckets[key] = bucket.filter((model) => model.id !== id) + } + componentModelsBuckets.value = updatedBuckets + showSuccess('Modèle de composant supprimé') + } + return result + } catch (error) { + showError(`Erreur lors de la suppression du modèle de composant: ${error.message}`) + return { success: false, error: error.message } + } finally { + loadingComponentModels.value = false + } + } + + const allComponentModels = computed(() => { + return Object.values(componentModelsBuckets.value).reduce((acc, bucket) => { + bucket.forEach((model) => { + if (!acc.find((existing) => existing.id === model.id)) { + acc.push(model) + } + }) + return acc + }, []) + }) + + const getComponentModelsForType = (typeComposantId) => { + return componentModelsBuckets.value[typeComposantId] || [] + } + + const getComponentModels = () => allComponentModels.value + const isComponentModelLoading = () => loadingComponentModels.value + + return { + componentModels: allComponentModels, + componentModelsBuckets, + loadingComponentModels, + loadComponentModels, + createComponentModel, + updateComponentModel, + deleteComponentModel, + getComponentModels, + getComponentModelsForType, + isComponentModelLoading, + } +} diff --git a/app/composables/useComponentTypes.js b/app/composables/useComponentTypes.js new file mode 100644 index 0000000..2576166 --- /dev/null +++ b/app/composables/useComponentTypes.js @@ -0,0 +1,95 @@ +import { ref } from 'vue' +import { useApi } from './useApi' +import { useToast } from './useToast' + +const componentTypes = ref([]) +const loadingComponentTypes = ref(false) + +export function useComponentTypes() { + const { get, post, patch, delete: del } = useApi() + const { showSuccess, showError } = useToast() + + const loadComponentTypes = async () => { + loadingComponentTypes.value = true + try { + const result = await get('/types/composants') + if (result.success) { + componentTypes.value = result.data + } + return result + } catch (error) { + showError(`Impossible de charger les types de composant: ${error.message}`) + return { success: false, error: error.message } + } finally { + loadingComponentTypes.value = false + } + } + + const createComponentType = async (payload) => { + loadingComponentTypes.value = true + try { + const result = await post('/types/composants', payload) + if (result.success) { + componentTypes.value.push(result.data) + showSuccess(`Type de composant "${result.data.name}" créé`) + } + return result + } catch (error) { + showError(`Erreur lors de la création du type de composant: ${error.message}`) + return { success: false, error: error.message } + } finally { + loadingComponentTypes.value = false + } + } + + const updateComponentType = async (id, payload) => { + loadingComponentTypes.value = true + try { + const result = await patch(`/types/composants/${id}`, payload) + if (result.success) { + const index = componentTypes.value.findIndex((type) => type.id === id) + if (index !== -1) { + componentTypes.value[index] = result.data + } + showSuccess(`Type de composant "${result.data.name}" mis à jour`) + } + return result + } catch (error) { + showError(`Erreur lors de la mise à jour du type de composant: ${error.message}`) + return { success: false, error: error.message } + } finally { + loadingComponentTypes.value = false + } + } + + const deleteComponentType = async (id) => { + loadingComponentTypes.value = true + try { + const result = await del(`/types/composants/${id}`) + if (result.success) { + componentTypes.value = componentTypes.value.filter((type) => type.id !== id) + showSuccess('Type de composant supprimé') + } + return result + } catch (error) { + showError(`Erreur lors de la suppression du type de composant: ${error.message}`) + return { success: false, error: error.message } + } finally { + loadingComponentTypes.value = false + } + } + + const getComponentTypes = () => componentTypes.value + const isComponentTypeLoading = () => loadingComponentTypes.value + + return { + componentTypes, + loadingComponentTypes, + loadComponentTypes, + createComponentType, + updateComponentType, + deleteComponentType, + getComponentTypes, + isComponentTypeLoading, + } +} diff --git a/app/composables/useMachineTypes.js b/app/composables/useMachineTypes.js deleted file mode 100644 index d81d0e7..0000000 --- a/app/composables/useMachineTypes.js +++ /dev/null @@ -1,654 +0,0 @@ -import { ref } from 'vue' - -// Types de machines prédéfinis avec structure hiérarchique -const machineTypes = ref([ - // Machines de production - { - id: 1, - name: 'Presse hydraulique', - category: 'Production', - description: 'Machine de formage par compression hydraulique', - maintenanceFrequency: 'Mensuelle', - components: [ - { - name: 'Système hydraulique', - subComponents: [ - { - name: 'Pompe hydraulique', - subComponents: [ - { name: 'Rotor' }, - { name: 'Stator' }, - { name: 'Joint d\'étanchéité' } - ] - }, - { - name: 'Cylindre principal', - subComponents: [ - { name: 'Piston' }, - { name: 'Tige' }, - { name: 'Joint de piston' } - ] - }, - { - name: 'Soupapes de sécurité', - subComponents: [ - { name: 'Soupape de surpression' }, - { name: 'Soupape de décharge' } - ] - } - ] - }, - { - name: 'Système mécanique', - subComponents: [ - { - name: 'Banc de machine', - subComponents: [ - { name: 'Poutre supérieure' }, - { name: 'Poutre inférieure' }, - { name: 'Colonnes' } - ] - }, - { - name: 'Système de guidage', - subComponents: [ - { name: 'Rails de guidage' }, - { name: 'Patins' } - ] - } - ] - } - ], - criticalParts: ['Pompe hydraulique', 'Cylindre principal', 'Soupapes de sécurité'], - specifications: { - force: '100-5000 tonnes', - course: '100-800 mm', - vitesse: '5-50 mm/s' - } - }, - { - id: 2, - name: 'Convoyeur à bande', - category: 'Production', - description: 'Système de transport continu de matériaux', - maintenanceFrequency: 'Hebdomadaire', - components: [ - { - name: 'Système de transport', - subComponents: [ - { - name: 'Bande transporteuse', - subComponents: [ - { name: 'Carcasse' }, - { name: 'Revêtement' }, - { name: 'Armature' } - ] - }, - { - name: 'Rouleaux', - subComponents: [ - { name: 'Rouleaux porteurs' }, - { name: 'Rouleaux de retour' }, - { name: 'Rouleaux d\'impact' } - ] - } - ] - }, - { - name: 'Système d\'entraînement', - subComponents: [ - { - name: 'Moteur d\'entraînement', - subComponents: [ - { name: 'Rotor' }, - { name: 'Stator' }, - { name: 'Roulements' } - ] - }, - { - name: 'Réducteur', - subComponents: [ - { name: 'Engrenages' }, - { name: 'Arbre de sortie' } - ] - } - ] - } - ], - criticalParts: ['Bande transporteuse', 'Rouleaux', 'Moteur d\'entraînement'], - specifications: { - longueur: '5-100 m', - largeur: '400-2000 mm', - vitesse: '0.5-3 m/s' - } - }, - { - id: 3, - name: 'Robot de soudage', - category: 'Production', - description: 'Robot industriel pour opérations de soudage automatisé', - maintenanceFrequency: 'Trimestrielle', - components: [ - { - name: 'Bras robotique', - subComponents: [ - { - name: 'Base rotative', - subComponents: [ - { name: 'Moteur de rotation' }, - { name: 'Réducteur' }, - { name: 'Capteur de position' } - ] - }, - { - name: 'Bras articulé', - subComponents: [ - { name: 'Joint 1' }, - { name: 'Joint 2' }, - { name: 'Joint 3' } - ] - } - ] - }, - { - name: 'Système de soudage', - subComponents: [ - { - name: 'Torche de soudage', - subComponents: [ - { name: 'Électrode' }, - { name: 'Gainage' }, - { name: 'Conduit de gaz' } - ] - }, - { - name: 'Alimentation électrique', - subComponents: [ - { name: 'Transformateur' }, - { name: 'Régulateur de courant' } - ] - } - ] - } - ], - criticalParts: ['Bras robotique', 'Torche de soudage', 'Contrôleur'], - specifications: { - portée: '1.5-3 m', - charge: '5-200 kg', - précision: '±0.1 mm' - } - }, - - // Machines de transformation - { - id: 4, - name: 'Tour CNC', - category: 'Transformation', - description: 'Machine-outil pour usinage de pièces cylindriques', - maintenanceFrequency: 'Mensuelle', - components: [ - { - name: 'Banc de machine', - subComponents: [ - { - name: 'Banc principal', - subComponents: [ - { name: 'Poutre' }, - { name: 'Guidages' }, - { name: 'Vis à billes' } - ] - } - ] - }, - { - name: 'Système de broche', - subComponents: [ - { - name: 'Broche principale', - subComponents: [ - { name: 'Arbre de broche' }, - { name: 'Roulements' }, - { name: 'Moteur de broche' } - ] - }, - { - name: 'Contre-pointe', - subComponents: [ - { name: 'Pointe' }, - { name: 'Cylindre' } - ] - } - ] - } - ], - criticalParts: ['Banc de machine', 'Broche', 'Contre-pointe'], - specifications: { - diamètre: '200-1000 mm', - longueur: '500-3000 mm', - puissance: '5-50 kW' - } - }, - { - id: 5, - name: 'Fraiseuse', - category: 'Transformation', - description: 'Machine-outil pour usinage par enlèvement de copeaux', - maintenanceFrequency: 'Mensuelle', - components: [ - { - name: 'Table de travail', - subComponents: [ - { - name: 'Table X', - subComponents: [ - { name: 'Guidages X' }, - { name: 'Vis à billes X' }, - { name: 'Moteur X' } - ] - }, - { - name: 'Table Y', - subComponents: [ - { name: 'Guidages Y' }, - { name: 'Vis à billes Y' }, - { name: 'Moteur Y' } - ] - } - ] - }, - { - name: 'Système de broche', - subComponents: [ - { - name: 'Broche verticale', - subComponents: [ - { name: 'Arbre de broche' }, - { name: 'Roulements' }, - { name: 'Moteur de broche' } - ] - } - ] - } - ], - criticalParts: ['Table de travail', 'Broche', 'Guidages'], - specifications: { - courseX: '400-2000 mm', - courseY: '300-1500 mm', - courseZ: '200-800 mm' - } - }, - - // Machines de manutention - { - id: 6, - name: 'Pont roulant', - category: 'Manutention', - description: 'Système de levage et transport de charges lourdes', - maintenanceFrequency: 'Mensuelle', - components: [ - { - name: 'Poutre principale', - subComponents: [ - { - name: 'Poutre de roulement', - subComponents: [ - { name: 'Profilé principal' }, - { name: 'Rails de roulement' }, - { name: 'Renforts' } - ] - } - ] - }, - { - name: 'Système de palans', - subComponents: [ - { - name: 'Palans', - subComponents: [ - { name: 'Moteur de levage' }, - { name: 'Treuil' }, - { name: 'Crochet' } - ] - }, - { - name: 'Système de translation', - subComponents: [ - { name: 'Moteur de translation' }, - { name: 'Roues de roulement' } - ] - } - ] - } - ], - criticalParts: ['Poutre principale', 'Palans', 'Rails de guidage'], - specifications: { - capacité: '1-500 tonnes', - portée: '5-50 m', - hauteur: '3-20 m' - } - }, - { - id: 7, - name: 'Chariot élévateur', - category: 'Manutention', - description: 'Véhicule de manutention pour charges palettisées', - maintenanceFrequency: 'Hebdomadaire', - components: [ - { - name: 'Système de levage', - subComponents: [ - { - name: 'Mast', - subComponents: [ - { name: 'Mât extérieur' }, - { name: 'Mât intérieur' }, - { name: 'Cylindres de levage' } - ] - }, - { - name: 'Fourches', - subComponents: [ - { name: 'Fourche gauche' }, - { name: 'Fourche droite' }, - { name: 'Système de réglage' } - ] - } - ] - }, - { - name: 'Groupe motopropulseur', - subComponents: [ - { - name: 'Moteur', - subComponents: [ - { name: 'Bloc moteur' }, - { name: 'Système d\'injection' }, - { name: 'Système de refroidissement' } - ] - }, - { - name: 'Transmission', - subComponents: [ - { name: 'Boîte de vitesses' }, - { name: 'Arbre de transmission' }, - { name: 'Pont arrière' } - ] - } - ] - } - ], - criticalParts: ['Mast', 'Fourches', 'Moteur'], - specifications: { - capacité: '1-10 tonnes', - hauteur: '3-6 m', - type: 'Électrique/Diesel/Gaz' - } - }, - - // Machines de traitement - { - id: 8, - name: 'Compresseur d\'air', - category: 'Traitement', - description: 'Générateur d\'air comprimé pour applications industrielles', - maintenanceFrequency: 'Hebdomadaire', - components: [ - { - name: 'Système de compression', - subComponents: [ - { - name: 'Compresseur', - subComponents: [ - { name: 'Pistons' }, - { name: 'Cylindres' }, - { name: 'Soupapes' } - ] - }, - { - name: 'Réservoir', - subComponents: [ - { name: 'Cuve' }, - { name: 'Soupape de sécurité' }, - { name: 'Manomètre' } - ] - } - ] - }, - { - name: 'Système de filtration', - subComponents: [ - { - name: 'Filtres', - subComponents: [ - { name: 'Filtre à air' }, - { name: 'Filtre à huile' }, - { name: 'Séparateur d\'eau' } - ] - } - ] - } - ], - criticalParts: ['Compresseur', 'Réservoir', 'Filtres'], - specifications: { - débit: '100-10000 L/min', - pression: '7-10 bar', - puissance: '5-500 kW' - } - }, - { - id: 9, - name: 'Pompe hydraulique', - category: 'Traitement', - description: 'Pompe pour circuits hydrauliques industriels', - maintenanceFrequency: 'Mensuelle', - components: [ - { - name: 'Système de pompage', - subComponents: [ - { - name: 'Rotor', - subComponents: [ - { name: 'Ailettes' }, - { name: 'Arbre' } - ] - }, - { - name: 'Stator', - subComponents: [ - { name: 'Corps' }, - { name: 'Chambres' } - ] - } - ] - }, - { - name: 'Système d\'étanchéité', - subComponents: [ - { - name: 'Joint d\'étanchéité', - subComponents: [ - { name: 'Joint radial' }, - { name: 'Joint axial' } - ] - } - ] - } - ], - criticalParts: ['Rotor', 'Stator', 'Joint d\'étanchéité'], - specifications: { - débit: '10-500 L/min', - pression: '50-350 bar', - type: 'Piston/Palette/Engrenage' - } - }, - - // Machines de contrôle - { - id: 10, - name: 'Capteur de température', - category: 'Contrôle', - description: 'Instrument de mesure de température industrielle', - maintenanceFrequency: 'Annuelle', - components: [ - { - name: 'Système de mesure', - subComponents: [ - { - name: 'Élément sensible', - subComponents: [ - { name: 'Fil de platine' }, - { name: 'Isolation' } - ] - }, - { - name: 'Câblage', - subComponents: [ - { name: 'Fils de connexion' }, - { name: 'Gaine de protection' } - ] - } - ] - }, - { - name: 'Système de transmission', - subComponents: [ - { - name: 'Transmetteur', - subComponents: [ - { name: 'Circuit électronique' }, - { name: 'Affichage' } - ] - } - ] - } - ], - criticalParts: ['Élément sensible', 'Câblage', 'Transmetteur'], - specifications: { - plage: '-50 à +500°C', - précision: '±0.5°C', - type: 'PT100/PT1000/Thermocouple' - } - }, - { - id: 11, - name: 'Manomètre', - category: 'Contrôle', - description: 'Instrument de mesure de pression', - maintenanceFrequency: 'Annuelle', - components: [ - { - name: 'Système de mesure', - subComponents: [ - { - name: 'Tube de Bourdon', - subComponents: [ - { name: 'Tube' }, - { name: 'Extrémité fixe' }, - { name: 'Extrémité mobile' } - ] - }, - { - name: 'Cadran', - subComponents: [ - { name: 'Échelle' }, - { name: 'Aiguille' } - ] - } - ] - }, - { - name: 'Système de connexion', - subComponents: [ - { - name: 'Joint', - subComponents: [ - { name: 'Joint d\'étanchéité' }, - { name: 'Filetage' } - ] - } - ] - } - ], - criticalParts: ['Tube de Bourdon', 'Cadran', 'Joint'], - specifications: { - plage: '0-600 bar', - précision: '±1%', - type: 'Analogique/Numérique' - } - } -]) - -// Catégories disponibles -const categories = ref([ - 'Production', - 'Transformation', - 'Manutention', - 'Traitement', - 'Contrôle' -]) - -export function useMachineTypes() { - const getTypes = () => machineTypes.value - - const getTypeById = (id) => { - return machineTypes.value.find(type => type.id === id) - } - - const getTypesByCategory = (category) => { - return machineTypes.value.filter(type => type.category === category) - } - - const getCategories = () => categories.value - - const addType = (newType) => { - const id = Math.max(...machineTypes.value.map(t => t.id)) + 1 - machineTypes.value.push({ - id, - ...newType - }) - } - - const updateType = (id, updatedType) => { - const index = machineTypes.value.findIndex(type => type.id === id) - if (index !== -1) { - machineTypes.value[index] = { ...machineTypes.value[index], ...updatedType } - } - } - - const deleteType = (id) => { - const index = machineTypes.value.findIndex(type => type.id === id) - if (index !== -1) { - machineTypes.value.splice(index, 1) - } - } - - // Méthodes pour la hiérarchie - const flattenComponents = (components, level = 0) => { - let flat = [] - components.forEach(comp => { - flat.push({ ...comp, level }) - if (comp.subComponents && comp.subComponents.length > 0) { - flat = flat.concat(flattenComponents(comp.subComponents, level + 1)) - } - }) - return flat - } - - const getComponentHierarchy = (typeId) => { - const type = getTypeById(typeId) - if (!type || !type.components) return [] - return flattenComponents(type.components) - } - - return { - getTypes, - getTypeById, - getTypesByCategory, - getCategories, - addType, - updateType, - deleteType, - flattenComponents, - getComponentHierarchy - } -} \ No newline at end of file diff --git a/app/composables/usePieceModels.js b/app/composables/usePieceModels.js new file mode 100644 index 0000000..d9d6586 --- /dev/null +++ b/app/composables/usePieceModels.js @@ -0,0 +1,131 @@ +import { ref, computed } from 'vue' +import { useApi } from './useApi' +import { useToast } from './useToast' + +const pieceModelsBuckets = ref({}) +const loadingPieceModels = ref(false) + +export function usePieceModels() { + const { get, post, patch, delete: del } = useApi() + const { showSuccess, showError } = useToast() + + const loadPieceModels = async (typePieceId) => { + loadingPieceModels.value = true + try { + const query = typePieceId ? `?typePieceId=${encodeURIComponent(typePieceId)}` : '' + const result = await get(`/types/pieces/models${query}`) + if (result.success) { + const key = typePieceId || '__all__' + pieceModelsBuckets.value = { + ...pieceModelsBuckets.value, + [key]: result.data, + } + } + return result + } catch (error) { + showError(`Impossible de charger les modèles de pièce: ${error.message}`) + return { success: false, error: error.message } + } finally { + loadingPieceModels.value = false + } + } + + const createPieceModel = async (payload) => { + loadingPieceModels.value = true + try { + const result = await post('/types/pieces/models', payload) + if (result.success) { + const key = result.data?.typePieceId || '__all__' + const bucket = pieceModelsBuckets.value[key] || [] + pieceModelsBuckets.value = { + ...pieceModelsBuckets.value, + [key]: [...bucket, result.data], + } + showSuccess(`Modèle de pièce "${result.data.name}" créé`) + } + return result + } catch (error) { + showError(`Erreur lors de la création du modèle de pièce: ${error.message}`) + return { success: false, error: error.message } + } finally { + loadingPieceModels.value = false + } + } + + const updatePieceModel = async (id, payload) => { + loadingPieceModels.value = true + try { + const result = await patch(`/types/pieces/models/${id}`, payload) + if (result.success) { + const key = result.data?.typePieceId || '__all__' + const bucket = pieceModelsBuckets.value[key] || [] + const updatedBucket = bucket.map((model) => + model.id === id ? result.data : model + ) + pieceModelsBuckets.value = { + ...pieceModelsBuckets.value, + [key]: updatedBucket, + } + showSuccess(`Modèle de pièce "${result.data.name}" mis à jour`) + } + return result + } catch (error) { + showError(`Erreur lors de la mise à jour du modèle de pièce: ${error.message}`) + return { success: false, error: error.message } + } finally { + loadingPieceModels.value = false + } + } + + const deletePieceModel = async (id) => { + loadingPieceModels.value = true + try { + const result = await del(`/types/pieces/models/${id}`) + if (result.success) { + const updatedBuckets = {} + for (const [key, bucket] of Object.entries(pieceModelsBuckets.value)) { + updatedBuckets[key] = bucket.filter((model) => model.id !== id) + } + pieceModelsBuckets.value = updatedBuckets + showSuccess('Modèle de pièce supprimé') + } + return result + } catch (error) { + showError(`Erreur lors de la suppression du modèle de pièce: ${error.message}`) + return { success: false, error: error.message } + } finally { + loadingPieceModels.value = false + } + } + + const allPieceModels = computed(() => { + return Object.values(pieceModelsBuckets.value).reduce((acc, bucket) => { + bucket.forEach((model) => { + if (!acc.find((existing) => existing.id === model.id)) { + acc.push(model) + } + }) + return acc + }, []) + }) + + const getPieceModelsForType = (typePieceId) => { + return pieceModelsBuckets.value[typePieceId] || [] + } + + const getPieceModels = () => allPieceModels.value + const isPieceModelLoading = () => loadingPieceModels.value + + return { + pieceModels: allPieceModels, + pieceModelsBuckets, + loadingPieceModels, + loadPieceModels, + createPieceModel, + updatePieceModel, + deletePieceModel, + getPieceModels, + getPieceModelsForType, + isPieceModelLoading, + } +} diff --git a/app/composables/usePieceTypes.js b/app/composables/usePieceTypes.js new file mode 100644 index 0000000..efdf0c1 --- /dev/null +++ b/app/composables/usePieceTypes.js @@ -0,0 +1,95 @@ +import { ref } from 'vue' +import { useApi } from './useApi' +import { useToast } from './useToast' + +const pieceTypes = ref([]) +const loadingPieceTypes = ref(false) + +export function usePieceTypes() { + const { get, post, patch, delete: del } = useApi() + const { showSuccess, showError } = useToast() + + const loadPieceTypes = async () => { + loadingPieceTypes.value = true + try { + const result = await get('/types/pieces') + if (result.success) { + pieceTypes.value = result.data + } + return result + } catch (error) { + showError(`Impossible de charger les types de pièce: ${error.message}`) + return { success: false, error: error.message } + } finally { + loadingPieceTypes.value = false + } + } + + const createPieceType = async (payload) => { + loadingPieceTypes.value = true + try { + const result = await post('/types/pieces', payload) + if (result.success) { + pieceTypes.value.push(result.data) + showSuccess(`Type de pièce "${result.data.name}" créé`) + } + return result + } catch (error) { + showError(`Erreur lors de la création du type de pièce: ${error.message}`) + return { success: false, error: error.message } + } finally { + loadingPieceTypes.value = false + } + } + + const updatePieceType = async (id, payload) => { + loadingPieceTypes.value = true + try { + const result = await patch(`/types/pieces/${id}`, payload) + if (result.success) { + const index = pieceTypes.value.findIndex((type) => type.id === id) + if (index !== -1) { + pieceTypes.value[index] = result.data + } + showSuccess(`Type de pièce "${result.data.name}" mis à jour`) + } + return result + } catch (error) { + showError(`Erreur lors de la mise à jour du type de pièce: ${error.message}`) + return { success: false, error: error.message } + } finally { + loadingPieceTypes.value = false + } + } + + const deletePieceType = async (id) => { + loadingPieceTypes.value = true + try { + const result = await del(`/types/pieces/${id}`) + if (result.success) { + pieceTypes.value = pieceTypes.value.filter((type) => type.id !== id) + showSuccess('Type de pièce supprimé') + } + return result + } catch (error) { + showError(`Erreur lors de la suppression du type de pièce: ${error.message}`) + return { success: false, error: error.message } + } finally { + loadingPieceTypes.value = false + } + } + + const getPieceTypes = () => pieceTypes.value + const isPieceTypeLoading = () => loadingPieceTypes.value + + return { + pieceTypes, + loadingPieceTypes, + loadPieceTypes, + createPieceType, + updatePieceType, + deletePieceType, + getPieceTypes, + isPieceTypeLoading, + } +} diff --git a/app/pages/generator.vue b/app/pages/generator.vue index 13a186e..3a27b8c 100644 --- a/app/pages/generator.vue +++ b/app/pages/generator.vue @@ -51,13 +51,13 @@

    {{ type.description || 'Aucune description' }}

    - +
    @@ -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 = () => { diff --git a/app/pages/index.vue b/app/pages/index.vue index 3e2672b..c8470c3 100644 --- a/app/pages/index.vue +++ b/app/pages/index.vue @@ -372,12 +372,12 @@

    Structure du type sélectionné :

    - Composants : - {{ selectedMachineType.components?.length || 0 }} + Familles de composants : + {{ selectedMachineType.componentRequirements?.length || 0 }}
    - Pièces critiques : - {{ selectedMachineType.criticalParts?.length || 0 }} + Groupes de pièces : + {{ selectedMachineType.pieceRequirements?.length || 0 }}
    Catégorie : diff --git a/app/pages/machine/[id].vue b/app/pages/machine/[id].vue index 898bfec..9be742e 100644 --- a/app/pages/machine/[id].vue +++ b/app/pages/machine/[id].vue @@ -300,6 +300,99 @@
    + +
    +
    +
    +

    Structure sélectionnée

    +

    + Synthèse des familles définies dans le type et des modèles utilisés pour cette machine. +

    +
    + +
    +

    Composants

    +
    +
    +
    +

    + {{ group.requirement.label || group.requirement.typeComposant?.name || 'Famille de composants' }} +

    +

    + Type : {{ group.requirement.typeComposant?.name || 'Non défini' }} · Min {{ group.requirement.minCount ?? (group.requirement.required ? 1 : 0) }} · Max {{ group.requirement.maxCount ?? '∞' }} +

    +
    + {{ group.components.length }} composant(s) +
    + +
    +
    + {{ component.name }} + + Modèle : {{ component.composantModel.name }} + + Défini manuellement + + (Sous-composant) + +
    +
    +

    Aucun composant rattaché à ce groupe.

    +
    +
    + +
    +

    Pièces principales

    +
    +
    +
    +

    + {{ group.requirement.label || group.requirement.typePiece?.name || 'Groupe de pièces' }} +

    +

    + Type : {{ group.requirement.typePiece?.name || 'Non défini' }} · Min {{ group.requirement.minCount ?? (group.requirement.required ? 1 : 0) }} · Max {{ group.requirement.maxCount ?? '∞' }} +

    +
    + {{ group.pieces.length }} pièce(s) +
    + +
    +
    + {{ piece.name }} + + Modèle : {{ piece.pieceModel.name }} + + Définie manuellement + + (Rattachée à {{ piece.parentComponentName }}) + +
    +
    +

    Aucune pièce rattachée à ce groupe.

    +
    +
    +
    +
    +
    @@ -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" />
    @@ -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" /> @@ -378,7 +479,73 @@ @select-all="setAllPrintSelection(true)" @deselect-all="setAllPrintSelection(false)" /> - + + + + diff --git a/app/pages/models.vue b/app/pages/models.vue new file mode 100644 index 0000000..d39ef31 --- /dev/null +++ b/app/pages/models.vue @@ -0,0 +1,585 @@ + + + diff --git a/app/pages/type/[id].vue b/app/pages/type/[id].vue index 396db62..17cc9d9 100644 --- a/app/pages/type/[id].vue +++ b/app/pages/type/[id].vue @@ -29,49 +29,63 @@ -
    - -
    - - -
    -

    Composants existants

    -
    - + +
    +

    Familles de composants

    +
    +
    +
    +
    +

    + {{ requirement.label || requirement.typeComposant?.name || 'Famille' }} +

    +

    + Type : {{ requirement.typeComposant?.name || 'Non défini' }} +

    +
    + + Min {{ toDisplayCount(requirement.minCount, requirement.required ? 1 : 0) }} • + Max {{ toDisplayCount(requirement.maxCount, '∞') }} + +
    +

    + {{ requirement.allowNewModels ? 'Création de modèles autorisée' : 'Modèles existants uniquement' }} +

    +
    - -
    -

    Pièces principales existantes

    -
    - + +
    +

    Groupes de pièces

    +
    +
    +
    +
    +

    + {{ requirement.label || requirement.typePiece?.name || 'Groupe' }} +

    +

    + Type : {{ requirement.typePiece?.name || 'Non défini' }} +

    +
    + + Min {{ toDisplayCount(requirement.minCount, requirement.required ? 1 : 0) }} • + Max {{ toDisplayCount(requirement.maxCount, '∞') }} + +
    +

    + {{ requirement.allowNewModels ? 'Création de modèles autorisée' : 'Modèles existants uniquement' }} +

    +
    @@ -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 () => { diff --git a/app/pages/type/edit/[id].vue b/app/pages/type/edit/[id].vue index b704110..52203a9 100644 --- a/app/pages/type/edit/[id].vue +++ b/app/pages/type/edit/[id].vue @@ -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) diff --git a/app/pages/types.vue b/app/pages/types.vue index e279958..5507227 100644 --- a/app/pages/types.vue +++ b/app/pages/types.vue @@ -42,13 +42,14 @@
    {{ type.category }}

    {{ type.description }}

    -
    -
    -