-
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.machinePieces?.length || 0 }} pièces totales
+
+
+
+ {{ type.componentRequirements?.length || 0 }} famille(s) de composants
+
+
+
+ {{ type.pieceRequirements?.length || 0 }} groupe(s) de pièces
diff --git a/app/shared/modelUtils.ts b/app/shared/modelUtils.ts
new file mode 100644
index 0000000..0401d1e
--- /dev/null
+++ b/app/shared/modelUtils.ts
@@ -0,0 +1,289 @@
+export const isPlainObject = (value: unknown): value is Record => {
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
+}
+
+export interface ModelStructurePreview {
+ customFields: number
+ pieces: number
+ subComponents: number
+}
+
+export const defaultStructure = () => ({
+ customFields: [],
+ pieces: [],
+ subComponents: [],
+})
+
+export const cloneStructure = (input: any) => {
+ try {
+ return JSON.parse(JSON.stringify(input ?? defaultStructure()))
+ } catch (error) {
+ return defaultStructure()
+ }
+}
+
+const sanitizeCustomFields = (fields: any[]): any[] => {
+ if (!Array.isArray(fields)) {
+ return []
+ }
+
+ return fields
+ .map((field) => {
+ const name = typeof field?.name === 'string' ? field.name.trim() : ''
+ if (!name) {
+ return null
+ }
+
+ const type = typeof field?.type === 'string' && field.type ? field.type : 'text'
+ const required = !!field?.required
+ const defaultValue = typeof field?.defaultValue === 'string' && field.defaultValue.trim().length > 0
+ ? field.defaultValue.trim()
+ : undefined
+
+ let options: string[] | undefined
+ if (type === 'select') {
+ const rawOptions = typeof field?.optionsText === 'string'
+ ? field.optionsText
+ : Array.isArray(field?.options)
+ ? field.options.join('\n')
+ : ''
+ const parsed = rawOptions
+ .split(/\r?\n/)
+ .map((option) => option.trim())
+ .filter((option) => option.length > 0)
+ options = parsed.length > 0 ? parsed : undefined
+ }
+
+ const result: Record = { name, type, required }
+ if (defaultValue !== undefined) {
+ result.defaultValue = defaultValue
+ }
+ if (options) {
+ result.options = options
+ }
+ return result
+ })
+ .filter(Boolean)
+}
+
+const sanitizePieces = (pieces: any[]): any[] => {
+ if (!Array.isArray(pieces)) {
+ return []
+ }
+
+ return pieces
+ .map((piece) => {
+ const name = typeof piece?.name === 'string' ? piece.name.trim() : ''
+ if (!name) {
+ return null
+ }
+
+ const reference = typeof piece?.reference === 'string' && piece.reference.trim().length > 0
+ ? piece.reference.trim()
+ : undefined
+
+ const quantity = Number(piece?.quantity)
+ const normalizedQuantity = Number.isFinite(quantity) && quantity > 0 ? quantity : undefined
+
+ const result: Record = { name }
+ if (reference !== undefined) {
+ result.reference = reference
+ }
+ if (normalizedQuantity !== undefined) {
+ result.quantity = normalizedQuantity
+ }
+ return result
+ })
+ .filter(Boolean)
+}
+
+const sanitizeSubComponents = (components: any[]): any[] => {
+ if (!Array.isArray(components)) {
+ return []
+ }
+
+ return components
+ .map((component) => {
+ const name = typeof component?.name === 'string' ? component.name.trim() : ''
+ if (!name) {
+ return null
+ }
+
+ const description = typeof component?.description === 'string' && component.description.trim().length > 0
+ ? component.description.trim()
+ : undefined
+
+ const quantity = Number(component?.quantity)
+ const normalizedQuantity = Number.isFinite(quantity) && quantity > 0 ? quantity : undefined
+
+ const customFields = sanitizeCustomFields(component?.customFields)
+ const pieces = sanitizePieces(component?.pieces)
+ const subComponents = sanitizeSubComponents(component?.subComponents)
+
+ const result: Record = {
+ name,
+ customFields,
+ pieces,
+ subComponents,
+ }
+
+ if (description !== undefined) {
+ result.description = description
+ }
+ if (normalizedQuantity !== undefined) {
+ result.quantity = normalizedQuantity
+ }
+
+ return result
+ })
+ .filter(Boolean)
+}
+
+export const normalizeStructureForSave = (input: any) => {
+ const source = cloneStructure(input)
+
+ return {
+ customFields: sanitizeCustomFields(source.customFields),
+ pieces: sanitizePieces(source.pieces),
+ subComponents: sanitizeSubComponents(source.subComponents),
+ }
+}
+
+const hydrateCustomFields = (fields: any[]): any[] => {
+ if (!Array.isArray(fields)) {
+ return []
+ }
+
+ return fields.map((field) => ({
+ name: field?.name ?? '',
+ type: field?.type ?? 'text',
+ required: !!field?.required,
+ defaultValue: field?.defaultValue ?? '',
+ options: Array.isArray(field?.options) ? field.options : [],
+ optionsText: Array.isArray(field?.options) ? field.options.join('\n') : (field?.optionsText ?? ''),
+ }))
+}
+
+const hydratePieces = (pieces: any[]): any[] => {
+ if (!Array.isArray(pieces)) {
+ return []
+ }
+
+ return pieces.map((piece) => ({
+ name: piece?.name ?? '',
+ reference: piece?.reference ?? '',
+ quantity: piece?.quantity ?? undefined,
+ }))
+}
+
+const hydrateSubComponents = (components: any[]): any[] => {
+ if (!Array.isArray(components)) {
+ return []
+ }
+
+ return components.map((component) => ({
+ name: component?.name ?? '',
+ description: component?.description ?? '',
+ quantity: component?.quantity ?? undefined,
+ customFields: hydrateCustomFields(component?.customFields),
+ pieces: hydratePieces(component?.pieces),
+ subComponents: hydrateSubComponents(component?.subComponents),
+ }))
+}
+
+export const hydrateStructureForEditor = (input: any) => {
+ const source = cloneStructure(input)
+ return {
+ customFields: hydrateCustomFields(source.customFields),
+ pieces: hydratePieces(source.pieces),
+ subComponents: hydrateSubComponents(source.subComponents),
+ }
+}
+
+const toOptionsText = (field: any) => {
+ if (typeof field?.optionsText === 'string') {
+ return field.optionsText
+ }
+ if (Array.isArray(field?.options)) {
+ return field.options.join('\n')
+ }
+ return ''
+}
+
+const mapComponentCustomFields = (fields: any[]) => {
+ if (!Array.isArray(fields)) {
+ return []
+ }
+ return fields.map((field) => ({
+ name: field?.name ?? '',
+ type: field?.type ?? 'text',
+ required: !!field?.required,
+ defaultValue: field?.defaultValue ?? '',
+ options: Array.isArray(field?.options) ? field.options : [],
+ optionsText: toOptionsText(field),
+ }))
+}
+
+const mapComponentPieces = (pieces: any[]) => {
+ if (!Array.isArray(pieces)) {
+ return []
+ }
+ return pieces.map((piece) => ({
+ name: piece?.name ?? '',
+ reference: piece?.reference ?? '',
+ quantity: piece?.quantity ?? piece?.quantite ?? undefined,
+ }))
+}
+
+const mapSubComponents = (components: any[]): any[] => {
+ if (!Array.isArray(components)) {
+ return []
+ }
+ return components.map((component) => ({
+ name: component?.name ?? '',
+ description: component?.description ?? '',
+ quantity: component?.quantity ?? component?.quantite ?? undefined,
+ customFields: mapComponentCustomFields(component?.customFields),
+ pieces: mapComponentPieces(component?.pieces),
+ subComponents: mapSubComponents(component?.subComponents),
+ }))
+}
+
+export const extractStructureFromComponent = (component: any) => {
+ if (!component) {
+ return defaultStructure()
+ }
+
+ const raw = {
+ customFields: mapComponentCustomFields(component.customFields),
+ pieces: mapComponentPieces(component.pieces),
+ subComponents: mapSubComponents(component.subComponents),
+ }
+
+ return normalizeStructureForSave(raw)
+}
+
+export const computeStructureStats = (structure: any): ModelStructurePreview => {
+ if (!structure || typeof structure !== 'object') {
+ return { customFields: 0, pieces: 0, subComponents: 0 }
+ }
+
+ return {
+ customFields: Array.isArray(structure.customFields) ? structure.customFields.length : 0,
+ pieces: Array.isArray(structure.pieces) ? structure.pieces.length : 0,
+ subComponents: Array.isArray(structure.subComponents) ? structure.subComponents.length : 0,
+ }
+}
+
+export const formatStructurePreview = (structure: any) => {
+ const stats = computeStructureStats(structure)
+ if (!stats.customFields && !stats.pieces && !stats.subComponents) {
+ return 'Structure vide'
+ }
+
+ const segments: string[] = []
+ if (stats.customFields) segments.push(`${stats.customFields} champ(s)`)
+ if (stats.pieces) segments.push(`${stats.pieces} pièce(s)`)
+ if (stats.subComponents) segments.push(`${stats.subComponents} sous-composant(s)`)
+ return segments.join(' • ')
+}