/** * Pure functions for custom field resolution, merging, and deduplication. * * Extracted from ComponentItem.vue and PieceItem.vue which had ~350 LOC * of identical custom field logic duplicated between them. */ // --------------------------------------------------------------------------- // Field key / identity helpers // --------------------------------------------------------------------------- export function fieldKeyFromNameAndType(name: unknown, type: unknown): string | null { const normalizedName = typeof name === 'string' ? name.trim().toLowerCase() : '' const normalizedType = typeof type === 'string' ? type.trim().toLowerCase() : '' return normalizedName ? `${normalizedName}::${normalizedType}` : null } export function resolveOrderIndex(field: any): number { if (!field || typeof field !== 'object') return 0 if (typeof field.orderIndex === 'number') return field.orderIndex if (field.customField && typeof field.customField.orderIndex === 'number') return field.customField.orderIndex return 0 } // --------------------------------------------------------------------------- // Field accessors // --------------------------------------------------------------------------- export function resolveFieldKey(field: any, index: number): string { return field?.id ?? field?.customFieldValueId ?? field?.customFieldId ?? field?.name ?? `field-${index}` } export function resolveFieldId(field: any): string | null { return field?.customFieldValueId ?? null } export function resolveFieldName(field: any): string { return field?.name ?? 'Champ' } export function resolveFieldType(field: any): string { return field?.type ?? 'text' } export function resolveFieldOptions(field: any): string[] { return field?.options ?? [] } export function resolveFieldRequired(field: any): boolean { return !!field?.required } export function resolveFieldReadOnly(field: any): boolean { return !!field?.readOnly } export function resolveCustomFieldId(field: any): string | null { return field?.customFieldId ?? field?.id ?? field?.customField?.id ?? null } export function buildCustomFieldMetadata(field: any) { return { customFieldName: resolveFieldName(field), customFieldType: resolveFieldType(field), customFieldRequired: resolveFieldRequired(field), customFieldOptions: resolveFieldOptions(field), } } export function formatFieldDisplayValue(field: any): string { const type = resolveFieldType(field) const rawValue = field?.value ?? '' if (type === 'boolean') { const normalized = String(rawValue).toLowerCase() if (normalized === 'true') return 'Oui' if (normalized === 'false') return 'Non' } return rawValue || 'Non défini' } // --------------------------------------------------------------------------- // Custom field ID resolution against candidate pool // --------------------------------------------------------------------------- export function ensureCustomFieldId(field: any, candidateFields: any[]): string | null { const existingId = resolveCustomFieldId(field) if (existingId) return existingId const name = resolveFieldName(field) if (!name || name === 'Champ') return null const matches = candidateFields.filter((candidate) => { if (!candidate || typeof candidate !== 'object') return false const candidateId = candidate.id || candidate.customFieldId if (candidateId && (candidateId === field?.id || candidateId === field?.customFieldId)) return true return typeof candidate.name === 'string' && candidate.name === name }) if (matches.length) { const withId = matches.find((c) => c?.id || c?.customFieldId) || matches[0] const id = withId?.id || withId?.customFieldId || null if (id) field.customFieldId = id if (!field.customField && typeof withId === 'object') field.customField = withId return id } return null } // --------------------------------------------------------------------------- // Structure extraction // --------------------------------------------------------------------------- export function extractStructureCustomFields(structure: any): any[] { if (!structure || typeof structure !== 'object') return [] const customFields = structure.customFields return Array.isArray(customFields) ? customFields : [] } // --------------------------------------------------------------------------- // Deduplication & merge // --------------------------------------------------------------------------- export function deduplicateFieldDefinitions(definitions: any[]): any[] { const result: any[] = [] const seenIds = new Set() const seenNames = new Set() const orderedDefinitions = (Array.isArray(definitions) ? definitions.slice() : []) .sort((a, b) => resolveOrderIndex(a) - resolveOrderIndex(b)) orderedDefinitions.forEach((field) => { if (!field || typeof field !== 'object') return const id = field.id ?? field.customFieldId ?? field.customField?.id ?? null const nameKey = fieldKeyFromNameAndType(field.name, field.type) // Deduplicate by name+type (primary) AND by id — a field with the same // name+type is the same field even when stored with different IDs. if (nameKey && seenNames.has(nameKey)) return if (id && seenIds.has(id)) return if (id) seenIds.add(id) if (nameKey) seenNames.add(nameKey) field.orderIndex = resolveOrderIndex(field) result.push(field) }) return result } export function mergeFieldDefinitionsWithValues(definitions: any[], values: any[]): any[] { const definitionList = Array.isArray(definitions) ? definitions : [] const valueList = Array.isArray(values) ? values : [] const valueMap = new Map() valueList.forEach((entry) => { if (!entry || typeof entry !== 'object') return const fieldId = entry.customField?.id ?? entry.customFieldId ?? null if (fieldId) valueMap.set(fieldId, entry) const nameKey = fieldKeyFromNameAndType(entry.customField?.name, entry.customField?.type) if (nameKey) valueMap.set(nameKey, entry) }) const merged = definitionList.map((field) => { if (!field || typeof field !== 'object') return field const fieldId = resolveCustomFieldId(field) const nameKey = fieldKeyFromNameAndType(resolveFieldName(field), resolveFieldType(field)) const matchedValue = (fieldId ? valueMap.get(fieldId) : undefined) ?? (nameKey ? valueMap.get(nameKey) : undefined) if (!matchedValue) { return { ...field, value: field?.value ?? '', orderIndex: resolveOrderIndex(field) } } const resolvedOrder = Math.min(resolveOrderIndex(field), resolveOrderIndex(matchedValue.customField)) return { ...field, customFieldValueId: matchedValue.id ?? field.customFieldValueId ?? null, customFieldId: matchedValue.customField?.id ?? matchedValue.customFieldId ?? fieldId ?? null, customField: matchedValue.customField ?? field.customField ?? null, value: matchedValue.value ?? field.value ?? '', orderIndex: resolvedOrder, } }) valueList.forEach((entry) => { if (!entry || typeof entry !== 'object') return const fieldId = entry.customField?.id ?? entry.customFieldId ?? null const nameKey = fieldKeyFromNameAndType(entry.customField?.name, entry.customField?.type) const exists = merged.some((field) => { if (!field || typeof field !== 'object') return false if (field.customFieldValueId && field.customFieldValueId === entry.id) return true const existingId = resolveCustomFieldId(field) if (fieldId && existingId && existingId === fieldId) return true if (!fieldId && nameKey) { return fieldKeyFromNameAndType(resolveFieldName(field), resolveFieldType(field)) === nameKey } return false }) if (!exists) { merged.push({ customFieldValueId: entry.id ?? null, customFieldId: fieldId, name: entry.customField?.name ?? '', type: entry.customField?.type ?? 'text', required: entry.customField?.required ?? false, options: entry.customField?.options ?? [], value: entry.value ?? '', customField: entry.customField ?? null, orderIndex: resolveOrderIndex(entry.customField), }) } }) return merged.sort((a, b) => resolveOrderIndex(a) - resolveOrderIndex(b)) } export function dedupeMergedFields(fields: any[]): any[] { if (!Array.isArray(fields) || fields.length <= 1) { return Array.isArray(fields) ? fields.slice().sort((a, b) => resolveOrderIndex(a) - resolveOrderIndex(b)) : [] } const seenById = new Map() const seenByName = new Map() const result: any[] = [] const orderedFields = fields.slice().sort((a, b) => resolveOrderIndex(a) - resolveOrderIndex(b)) orderedFields.forEach((field) => { if (!field || typeof field !== 'object') return const rawName = resolveFieldName(field) const normalizedName = typeof rawName === 'string' ? rawName.trim() : '' if (!normalizedName) return field.name = normalizedName field.type = field.type || resolveFieldType(field) const fieldId = resolveCustomFieldId(field) const nameKey = fieldKeyFromNameAndType(normalizedName, field.type) // Check duplicates by name+type first (same field can have different IDs) const existing = (nameKey ? seenByName.get(nameKey) : undefined) ?? (fieldId ? seenById.get(fieldId) : undefined) if (!existing) { field.orderIndex = resolveOrderIndex(field) if (fieldId) seenById.set(fieldId, field) if (nameKey) seenByName.set(nameKey, field) result.push(field) return } const existingHasValue = existing.value !== undefined && existing.value !== null && String(existing.value).trim().length > 0 const incomingHasValue = field.value !== undefined && field.value !== null && String(field.value).trim().length > 0 if (!existingHasValue && incomingHasValue) { Object.assign(existing, field) existing.orderIndex = Math.min(resolveOrderIndex(existing), resolveOrderIndex(field)) if (fieldId) seenById.set(fieldId, existing) if (nameKey) seenByName.set(nameKey, existing) } }) return result.sort((a, b) => resolveOrderIndex(a) - resolveOrderIndex(b)) } // --------------------------------------------------------------------------- // Definition sources builder // --------------------------------------------------------------------------- export function buildDefinitionSources(entity: any, entityType: 'composant' | 'piece'): any[] { const definitions: any[] = [] const pushFields = (collection: any) => { if (Array.isArray(collection)) definitions.push(...collection) } if (entityType === 'composant') { const type = entity.typeComposant || {} pushFields(entity.customFields) pushFields(entity.definition?.customFields) pushFields(type.customFields) ;[ entity.definition?.structure, type.structure, ].forEach((structure) => { const fields = extractStructureCustomFields(structure) if (fields.length) definitions.push(...fields) }) } else { const type = entity.typePiece || {} pushFields(entity.customFields) pushFields(entity.definition?.customFields) pushFields(type.customFields) ;[ entity.definition?.structure, type.structure, ].forEach((structure) => { const fields = extractStructureCustomFields(structure) if (fields.length) definitions.push(...fields) }) } return deduplicateFieldDefinitions(definitions) } // --------------------------------------------------------------------------- // Candidate fields builder // --------------------------------------------------------------------------- export function buildCandidateCustomFields(entity: any, definitionSources: any[]): any[] { const map = new Map() const register = (collection: any[]) => { if (!Array.isArray(collection)) return collection.forEach((item) => { if (!item || typeof item !== 'object') return const id = item.id || item.customFieldId const name = typeof item.name === 'string' ? item.name : null const key = id || (name ? `${name}::${item.type ?? ''}` : null) if (!key || map.has(key)) return map.set(key, item) }) } register((entity.customFieldValues || []).map((value: any) => value?.customField)) register(definitionSources) return Array.from(map.values()) }