/** * Machine detail — custom field management sub-composable. * * Handles custom field resolution, display filtering, sync and updates * for machines, components and pieces. */ import { ref, computed } from 'vue' import { useCustomFields } from '~/composables/useCustomFields' import { useToast } from '~/composables/useToast' import { mergeDefinitionsWithValues, filterByContext, hasDisplayableValue, type CustomFieldInput, } from '~/shared/utils/customFields' import { resolveConstructeurs, uniqueConstructeurIds, } from '~/shared/constructeurUtils' type AnyRecord = Record interface MachineDetailCustomFieldsDeps { machine: Ref isEditMode: Ref constructeurs: Ref resolveProductReference: (source: AnyRecord) => { product: unknown; productId: string | null } getProductDisplay: (source: AnyRecord) => unknown } export function useMachineDetailCustomFields(deps: MachineDetailCustomFieldsDeps) { const { machine, isEditMode, constructeurs, resolveProductReference, getProductDisplay } = deps const { upsertCustomFieldValue, updateCustomFieldValue: updateCustomFieldValueApi, } = useCustomFields() const toast = useToast() // --------------------------------------------------------------------------- // State // --------------------------------------------------------------------------- const machineCustomFields = ref([]) const pendingContextFieldUpdates = ref([]) // --------------------------------------------------------------------------- // Computed // --------------------------------------------------------------------------- const visibleMachineCustomFields = computed(() => { const fields = Array.isArray(machineCustomFields.value) ? machineCustomFields.value : [] if (isEditMode.value) return fields return fields.filter((field) => hasDisplayableValue(field as unknown as CustomFieldInput)) }) // --------------------------------------------------------------------------- // Transform helpers // --------------------------------------------------------------------------- const transformCustomFields = (piecesData: AnyRecord[]): AnyRecord[] => { return (piecesData || []).map((piece) => { const typePiece = (piece.typePiece as AnyRecord) || {} const customFields = filterByContext( mergeDefinitionsWithValues( typePiece.customFields ?? (piece.typePiece as AnyRecord)?.customFields ?? [], piece.customFieldValues ?? [], ), 'standalone', ) const constructeurIds = uniqueConstructeurIds( piece.constructeurs, piece.constructeurIds, piece.constructeurId, piece.constructeur, (piece.originalPiece as AnyRecord)?.constructeurs, (piece.originalPiece as AnyRecord)?.constructeurIds, (piece.originalPiece as AnyRecord)?.constructeurId, (piece.originalPiece as AnyRecord)?.constructeur, ) const { product: resolvedProduct, productId: resolvedProductId } = resolveProductReference(piece) const constructeursList = resolveConstructeurs( constructeurIds, Array.isArray(piece.constructeurs) ? (piece.constructeurs as any[]) : [], piece.constructeur ? [piece.constructeur as any] : [], Array.isArray((piece.originalPiece as AnyRecord)?.constructeurs) ? ((piece.originalPiece as AnyRecord).constructeurs as any[]) : [], (piece.originalPiece as AnyRecord)?.constructeur ? [(piece.originalPiece as AnyRecord).constructeur as any] : [], constructeurs.value as any, ) as any[] const normalizedPiece = { ...piece, product: resolvedProduct || piece.product || null, productId: resolvedProductId || piece.productId || (piece.product as AnyRecord)?.id || null, } const productDisplay = getProductDisplay(normalizedPiece) return { ...normalizedPiece, customFields: customFields.filter((f: any) => !f.machineContextOnly && !f.customField?.machineContextOnly), contextCustomFields: piece.contextCustomFields ?? [], contextCustomFieldValues: piece.contextCustomFieldValues ?? [], documents: piece.documents || [], constructeurs: constructeursList, constructeur: constructeursList[0] || piece.constructeur || null, constructeurIds, constructeurId: constructeurIds[0] || null, typePieceId: piece.typePieceId || (piece.typePiece as AnyRecord)?.id || null, __productDisplay: productDisplay, } }) } const transformComponentCustomFields = (componentsData: AnyRecord[]): AnyRecord[] => { return (componentsData || []).map((component) => { const type = (component.typeComposant as AnyRecord) || {} const actualComponent = (component.originalComposant as AnyRecord) || component const customFields = filterByContext( mergeDefinitionsWithValues( type.customFields ?? [], component.customFieldValues ?? actualComponent?.customFieldValues ?? [], ), 'standalone', ) const piecesTransformed = component.pieces ? transformCustomFields(component.pieces as AnyRecord[]).map((p) => ({ ...p, parentComponentName: component.name, })) : [] const subComponents = component.sousComposants ? transformComponentCustomFields(component.sousComposants as AnyRecord[]) : [] const constructeurIds = uniqueConstructeurIds( component.constructeurs, component.constructeurIds, component.constructeurId, component.constructeur, actualComponent?.constructeurs, actualComponent?.constructeurIds, actualComponent?.constructeurId, actualComponent?.constructeur, ) const constructeursList = resolveConstructeurs( constructeurIds, Array.isArray(component.constructeurs) ? (component.constructeurs as any[]) : [], component.constructeur ? [component.constructeur as any] : [], Array.isArray(actualComponent?.constructeurs) ? (actualComponent.constructeurs as any[]) : [], actualComponent?.constructeur ? [actualComponent.constructeur as any] : [], constructeurs.value as any, ) as any[] const { product: resolvedProduct, productId: resolvedProductId } = resolveProductReference(component) const normalizedComponent = { ...component, product: resolvedProduct || component.product || null, productId: resolvedProductId || component.productId || (component.product as AnyRecord)?.id || null, } const productDisplay = getProductDisplay(normalizedComponent) return { ...normalizedComponent, customFields: customFields.filter((f: any) => !f.machineContextOnly && !f.customField?.machineContextOnly), contextCustomFields: component.contextCustomFields ?? [], contextCustomFieldValues: component.contextCustomFieldValues ?? [], pieces: piecesTransformed, subComponents, documents: component.documents || [], constructeurs: constructeursList, constructeur: constructeursList[0] || component.constructeur || null, constructeurIds, constructeurId: constructeurIds[0] || null, typeComposantId: component.typeComposantId || (component.typeComposant as AnyRecord)?.id || null, __productDisplay: productDisplay, } }) } // --------------------------------------------------------------------------- // Machine custom field methods // --------------------------------------------------------------------------- const syncMachineCustomFields = () => { if (!machine.value) { machineCustomFields.value = [] return } const merged = mergeDefinitionsWithValues( machine.value?.customFields ?? [], machine.value?.customFieldValues ?? [], ) machineCustomFields.value = merged.map(f => ({ ...f, readOnly: false })) } const setMachineCustomFieldValue = (field: AnyRecord, value: unknown) => { if (!field) return field.value = value if (field.customFieldValueId && (machine.value as AnyRecord)?.customFieldValues) { const stored = ((machine.value as AnyRecord).customFieldValues as AnyRecord[]).find( (fv) => fv.id === field.customFieldValueId, ) if (stored) stored.value = value } } const updateMachineCustomField = async (field: AnyRecord) => { if (!machine.value || !field) return const customFieldId = (field.customFieldId ?? field.id) as string | undefined const customFieldValueId = field.customFieldValueId as string | undefined const fieldLabel = (field.name as string) || 'Champ personnalisé' try { if (customFieldValueId) { const result: any = await updateCustomFieldValueApi(customFieldValueId as string, { value: field.value ?? '', } as any) if (result.success) { toast.showSuccess(`Champ "${fieldLabel}" de la machine mis à jour avec succès`) syncMachineCustomFields() } else { toast.showError(`Erreur lors de la mise à jour du champ "${fieldLabel}"`) } return } if (!customFieldId) { toast.showError( 'Impossible de mettre à jour ce champ personnalisé (identifiant manquant).', ) return } const result: any = await upsertCustomFieldValue( customFieldId as string, 'machine', machine.value.id as string, field.value ?? '', ) if (result.success) { const createdValue = result.data as AnyRecord toast.showSuccess(`Champ "${fieldLabel}" de la machine mis à jour avec succès`) if (createdValue?.id) { if (!createdValue.customField) { createdValue.customField = { id: customFieldId, name: field.name, type: field.type, required: field.required, options: field.options, } } field.customFieldValueId = createdValue.id field.readOnly = false const existingValues = Array.isArray(machine.value.customFieldValues) ? (machine.value.customFieldValues as AnyRecord[]).filter( (item) => item.id !== createdValue.id, ) : [] machine.value.customFieldValues = [...existingValues, createdValue] } syncMachineCustomFields() } else { toast.showError(`Erreur lors de la mise à jour du champ "${fieldLabel}"`) } } catch (error) { console.error('Erreur lors de la mise à jour du champ personnalisé de la machine:', error) toast.showError(`Erreur lors de la mise à jour du champ "${fieldLabel}"`) } } const updatePieceCustomField = async (fieldUpdate: AnyRecord) => { try { const result: any = await upsertCustomFieldValue( fieldUpdate.fieldId as string, 'piece', fieldUpdate.pieceId as string, fieldUpdate.value, ) if (result.success) { toast.showSuccess('Champ personnalisé mis à jour avec succès') } else { toast.showError('Erreur lors de la mise à jour du champ personnalisé') } } catch (error) { toast.showError('Erreur lors de la mise à jour du champ personnalisé') console.error('Erreur lors de la mise à jour du champ personnalisé:', error) } } const handleCustomFieldUpdate = async (fieldUpdate: AnyRecord) => { if (fieldUpdate?.entityType && fieldUpdate?.entityId) { queueContextFieldUpdate(fieldUpdate) return } await updatePieceCustomField(fieldUpdate) } const queueContextFieldUpdate = (fieldUpdate: AnyRecord) => { const entityType = fieldUpdate.entityType as string | undefined const entityId = fieldUpdate.entityId as string | undefined const fieldId = fieldUpdate.fieldId as string | undefined const customFieldValueId = fieldUpdate.customFieldValueId as string | undefined if (!entityType || !entityId || (!fieldId && !customFieldValueId)) return const nextUpdate = { entityType, entityId, fieldId, customFieldValueId, value: fieldUpdate.value ?? '', fieldName: fieldUpdate.fieldName ?? 'Champ contextuel', } const existingIndex = pendingContextFieldUpdates.value.findIndex( (item) => item.entityType === entityType && item.entityId === entityId && item.fieldId === fieldId && item.customFieldValueId === customFieldValueId, ) if (existingIndex >= 0) { pendingContextFieldUpdates.value[existingIndex] = nextUpdate return } pendingContextFieldUpdates.value.push(nextUpdate) } const clearPendingContextFieldUpdates = () => { pendingContextFieldUpdates.value = [] } const saveAllContextCustomFields = async () => { const updates = pendingContextFieldUpdates.value.slice() if (!updates.length) return try { for (const update of updates) { if (update.customFieldValueId) { await updateCustomFieldValueApi(update.customFieldValueId as string, { value: update.value ?? '', } as any) continue } if (!update.fieldId) { continue } await upsertCustomFieldValue( update.fieldId as string, update.entityType as string, update.entityId as string, update.value ?? '', ) } clearPendingContextFieldUpdates() } catch (error) { console.error('Erreur lors de la sauvegarde batch des champs contextuels:', error) throw error } } const saveAllMachineCustomFields = async () => { if (!machine.value) return const fields = Array.isArray(machineCustomFields.value) ? machineCustomFields.value : [] const fieldsToSave = fields.filter( (field) => field.value !== undefined && field.value !== null && String(field.value).trim() !== '', ) for (const field of fieldsToSave) { const customFieldId = (field.customFieldId ?? field.id) as string | undefined const customFieldValueId = field.customFieldValueId as string | undefined try { if (customFieldValueId) { await updateCustomFieldValueApi(customFieldValueId as string, { value: field.value ?? '', } as any) } else if (customFieldId) { const result: any = await upsertCustomFieldValue( customFieldId as string, 'machine', machine.value.id as string, field.value ?? '', ) if (result.success) { const createdValue = result.data as AnyRecord if (createdValue?.id) { field.customFieldValueId = createdValue.id if (!createdValue.customField) { createdValue.customField = { id: customFieldId, name: field.name, type: field.type, required: field.required, options: field.options, } } const existingValues = Array.isArray(machine.value.customFieldValues) ? (machine.value.customFieldValues as AnyRecord[]).filter( (item) => item.id !== createdValue.id, ) : [] machine.value.customFieldValues = [...existingValues, createdValue] } } } } catch (error) { console.error('Erreur lors de la sauvegarde du champ personnalisé:', error) throw error } } } return { // State machineCustomFields, pendingContextFieldUpdates, // Computed visibleMachineCustomFields, // Transform functions transformCustomFields, transformComponentCustomFields, // Methods syncMachineCustomFields, setMachineCustomFieldValue, updateMachineCustomField, updatePieceCustomField, handleCustomFieldUpdate, queueContextFieldUpdate, clearPendingContextFieldUpdates, saveAllMachineCustomFields, saveAllContextCustomFields, } }