From 5a366595e6fb563ebc53672654a0bfb7d34c4bae Mon Sep 17 00:00:00 2001 From: Matthieu Date: Tue, 30 Sep 2025 15:35:32 +0200 Subject: [PATCH] feat: synchronize backend and frontend custom field handling --- src/common/constants/component-includes.ts | 64 +++ src/common/utils/component-tree.util.ts | 51 ++ src/composants/composants.service.ts | 620 +++++++-------------- src/machines/machines.service.ts | 186 ++++--- src/pieces/pieces.service.ts | 212 +++++-- 5 files changed, 613 insertions(+), 520 deletions(-) create mode 100644 src/common/constants/component-includes.ts create mode 100644 src/common/utils/component-tree.util.ts diff --git a/src/common/constants/component-includes.ts b/src/common/constants/component-includes.ts new file mode 100644 index 0000000..f7a89c2 --- /dev/null +++ b/src/common/constants/component-includes.ts @@ -0,0 +1,64 @@ +import { Prisma } from '@prisma/client'; + +const CUSTOM_FIELD_SELECT = { + id: true, + name: true, + type: true, + required: true, + options: true, +} as const; + +export const COMPONENT_WITH_RELATIONS_INCLUDE = { + machine: true, + parentComposant: true, + typeComposant: { + include: { + customFields: true, + }, + }, + composantModel: true, + typeMachineComponentRequirement: { + include: { + typeComposant: { + include: { + customFields: true, + }, + }, + }, + }, + constructeur: true, + customFieldValues: { + include: { + customField: { select: CUSTOM_FIELD_SELECT }, + }, + }, + pieces: { + include: { + customFieldValues: { + include: { + customField: { select: CUSTOM_FIELD_SELECT }, + }, + }, + constructeur: true, + pieceModel: true, + typeMachinePieceRequirement: { + include: { + typePiece: { + include: { + customFields: true, + }, + }, + }, + }, + documents: true, + }, + }, + documents: true, +} satisfies Prisma.ComposantInclude; + +export interface ComposantWithRelations + extends Prisma.ComposantGetPayload<{ + include: typeof COMPONENT_WITH_RELATIONS_INCLUDE; + }> { + sousComposants?: ComposantWithRelations[]; +} diff --git a/src/common/utils/component-tree.util.ts b/src/common/utils/component-tree.util.ts new file mode 100644 index 0000000..7a0bfcd --- /dev/null +++ b/src/common/utils/component-tree.util.ts @@ -0,0 +1,51 @@ +export interface HierarchicalComponent { + id: string; + parentComposantId?: string | null; + sousComposants?: T[]; +} + +export function buildComponentHierarchy>( + components: readonly T[], +): T[] { + if (!Array.isArray(components) || components.length === 0) { + return []; + } + + const byParent = new Map(); + + components.forEach((raw) => { + const component = raw as HierarchicalComponent; + const parentId = component.parentComposantId ?? null; + if (!byParent.has(parentId)) { + byParent.set(parentId, [] as T[]); + } + component.sousComposants = [] as T[]; + byParent.get(parentId)!.push(component as T); + }); + + const attach = (component: T): T => { + const children = byParent.get(component.id) ?? []; + component.sousComposants = children.map(attach); + return component; + }; + + const roots = byParent.get(null) ?? []; + return roots.map(attach); +} + +export function buildComponentSubtree>( + components: T[], + rootId: string, +): T | null { + if (!Array.isArray(components) || components.length === 0) { + return null; + } + + const map = new Map(); + components.forEach((component) => { + map.set(component.id, component); + }); + + buildComponentHierarchy(components); + return map.get(rootId) ?? null; +} diff --git a/src/composants/composants.service.ts b/src/composants/composants.service.ts index 61bcff8..6887f73 100644 --- a/src/composants/composants.service.ts +++ b/src/composants/composants.service.ts @@ -4,11 +4,52 @@ import { CreateComposantDto, UpdateComposantDto, } from '../shared/dto/composant.dto'; +import { + COMPONENT_WITH_RELATIONS_INCLUDE, + ComposantWithRelations, +} from '../common/constants/component-includes'; +import { + buildComponentHierarchy, + buildComponentSubtree, +} from '../common/utils/component-tree.util'; @Injectable() export class ComposantsService { constructor(private prisma: PrismaService) {} + private async fetchComponentsByMachine( + machineId: string, + ): Promise { + return this.prisma.composant.findMany({ + where: { machineId }, + include: COMPONENT_WITH_RELATIONS_INCLUDE, + }) as Promise; + } + + private async getComponentWithHierarchy( + id: string, + ): Promise { + const baseComponent = (await this.prisma.composant.findUnique({ + where: { id }, + include: COMPONENT_WITH_RELATIONS_INCLUDE, + })) as ComposantWithRelations | null; + + if (!baseComponent) { + return null; + } + + if (!baseComponent.machineId) { + baseComponent.sousComposants = []; + return baseComponent; + } + + const components = await this.fetchComponentsByMachine( + baseComponent.machineId, + ); + const subtree = buildComponentSubtree(components, id); + return subtree ?? baseComponent; + } + async create(createComposantDto: CreateComposantDto) { const requirementId = createComposantDto.typeMachineComponentRequirementId; @@ -77,434 +118,46 @@ export class ComposantsService { createComposantDto.typeComposantId ?? requirement.typeComposantId, }; - return this.prisma.composant.create({ + const created = (await this.prisma.composant.create({ data, - include: { - machine: true, - parentComposant: true, - typeComposant: true, - composantModel: true, - typeMachineComponentRequirement: { - include: { - typeComposant: true, - }, - }, - constructeur: true, - sousComposants: { - include: { - typeComposant: true, - composantModel: true, - typeMachineComponentRequirement: { - include: { - typeComposant: true, - }, - }, - pieces: { - include: { - pieceModel: true, - typeMachinePieceRequirement: { - include: { - typePiece: true, - }, - }, - }, - }, - }, - }, - pieces: { - include: { - pieceModel: true, - typeMachinePieceRequirement: { - include: { - typePiece: true, - }, - }, - }, - }, - documents: true, - }, - }); + include: COMPONENT_WITH_RELATIONS_INCLUDE, + })) as ComposantWithRelations; + + return this.getComponentWithHierarchy(created.id); } async findAll() { - return this.prisma.composant.findMany({ - include: { - machine: true, - parentComposant: true, - typeComposant: true, - composantModel: true, - typeMachineComponentRequirement: { - include: { - typeComposant: true, - }, - }, - constructeur: true, - customFieldValues: { - include: { - customField: true, - }, - }, - sousComposants: { - include: { - typeComposant: true, - composantModel: true, - typeMachineComponentRequirement: { - include: { - typeComposant: true, - }, - }, - customFieldValues: { - include: { - customField: true, - }, - }, - pieces: { - include: { - customFieldValues: { - include: { - customField: true, - }, - }, - constructeur: true, - pieceModel: true, - typeMachinePieceRequirement: { - include: { - typePiece: true, - }, - }, - }, - }, - constructeur: true, - }, - }, - pieces: { - include: { - customFieldValues: { - include: { - customField: true, - }, - }, - constructeur: true, - pieceModel: true, - typeMachinePieceRequirement: { - include: { - typePiece: true, - }, - }, - }, - }, - documents: true, - }, - }); + const components = (await this.prisma.composant.findMany({ + include: COMPONENT_WITH_RELATIONS_INCLUDE, + })) as ComposantWithRelations[]; + + return buildComponentHierarchy(components); } async findOne(id: string) { - return this.prisma.composant.findUnique({ - where: { id }, - include: { - machine: true, - parentComposant: true, - typeComposant: true, - composantModel: true, - typeMachineComponentRequirement: { - include: { - typeComposant: true, - }, - }, - constructeur: true, - customFieldValues: { - include: { - customField: true, - }, - }, - sousComposants: { - include: { - typeComposant: true, - composantModel: true, - typeMachineComponentRequirement: { - include: { - typeComposant: true, - }, - }, - customFieldValues: { - include: { - customField: true, - }, - }, - pieces: { - include: { - customFieldValues: { - include: { - customField: true, - }, - }, - constructeur: true, - pieceModel: true, - typeMachinePieceRequirement: { - include: { - typePiece: true, - }, - }, - }, - }, - constructeur: true, - }, - }, - pieces: { - include: { - customFieldValues: { - include: { - customField: true, - }, - }, - constructeur: true, - pieceModel: true, - typeMachinePieceRequirement: { - include: { - typePiece: true, - }, - }, - }, - }, - documents: true, - }, - }); + return this.getComponentWithHierarchy(id); } async findByMachine(machineId: string) { - return this.prisma.composant.findMany({ - where: { machineId }, - include: { - machine: true, - parentComposant: true, - typeComposant: true, - composantModel: true, - typeMachineComponentRequirement: { - include: { - typeComposant: true, - }, - }, - constructeur: true, - sousComposants: { - include: { - typeComposant: true, - composantModel: true, - typeMachineComponentRequirement: { - include: { - typeComposant: true, - }, - }, - pieces: true, - constructeur: true, - }, - }, - pieces: { - include: { - customFieldValues: { - include: { - customField: true, - }, - }, - constructeur: true, - pieceModel: true, - typeMachinePieceRequirement: { - include: { - typePiece: true, - }, - }, - }, - }, - customFieldValues: { - include: { - customField: true, - }, - }, - documents: true, - }, - }); + const components = await this.fetchComponentsByMachine(machineId); + return buildComponentHierarchy(components); } async findHierarchy(machineId: string) { - // Récupérer tous les composants de premier niveau (sans parent) - const rootComposants = await this.prisma.composant.findMany({ - where: { - machineId, - parentComposantId: null, - }, - include: { - typeComposant: true, - composantModel: true, - typeMachineComponentRequirement: { - include: { - typeComposant: true, - }, - }, - constructeur: true, - customFieldValues: { - include: { - customField: true, - }, - }, - sousComposants: { - include: { - typeComposant: true, - composantModel: true, - typeMachineComponentRequirement: { - include: { - typeComposant: true, - }, - }, - constructeur: true, - customFieldValues: { - include: { - customField: true, - }, - }, - pieces: { - include: { - customFieldValues: { - include: { - customField: true, - }, - }, - constructeur: true, - pieceModel: true, - typeMachinePieceRequirement: { - include: { - typePiece: true, - }, - }, - }, - }, - sousComposants: { - include: { - typeComposant: true, - composantModel: true, - typeMachineComponentRequirement: { - include: { - typeComposant: true, - }, - }, - constructeur: true, - customFieldValues: { - include: { - customField: true, - }, - }, - pieces: { - include: { - customFieldValues: { - include: { - customField: true, - }, - }, - constructeur: true, - pieceModel: true, - typeMachinePieceRequirement: { - include: { - typePiece: true, - }, - }, - }, - }, - }, - }, - }, - }, - pieces: { - include: { - customFieldValues: { - include: { - customField: true, - }, - }, - constructeur: true, - pieceModel: true, - typeMachinePieceRequirement: { - include: { - typePiece: true, - }, - }, - }, - }, - }, - }); - - return rootComposants; + const components = await this.fetchComponentsByMachine(machineId); + return buildComponentHierarchy(components); } async update(id: string, updateComposantDto: UpdateComposantDto) { - return this.prisma.composant.update({ + const updated = (await this.prisma.composant.update({ where: { id }, data: updateComposantDto, - include: { - machine: true, - parentComposant: true, - typeComposant: true, - composantModel: true, - typeMachineComponentRequirement: { - include: { - typeComposant: true, - }, - }, - constructeur: true, - customFieldValues: { - include: { - customField: true, - }, - }, - sousComposants: { - include: { - typeComposant: true, - composantModel: true, - typeMachineComponentRequirement: { - include: { - typeComposant: true, - }, - }, - constructeur: true, - customFieldValues: { - include: { - customField: true, - }, - }, - pieces: { - include: { - customFieldValues: { - include: { - customField: true, - }, - }, - constructeur: true, - pieceModel: true, - typeMachinePieceRequirement: { - include: { - typePiece: true, - }, - }, - }, - }, - }, - }, - pieces: { - include: { - customFieldValues: { - include: { - customField: true, - }, - }, - constructeur: true, - pieceModel: true, - typeMachinePieceRequirement: { - include: { - typePiece: true, - }, - }, - }, - }, - documents: true, - }, - }); + include: COMPONENT_WITH_RELATIONS_INCLUDE, + })) as ComposantWithRelations; + + await this.syncComponentModelCustomFields(updated); + + return this.getComponentWithHierarchy(updated.id); } private async resolveMachineIdFromComposant( @@ -543,4 +196,151 @@ export class ComposantsService { where: { id }, }); } + + private async syncComponentModelCustomFields( + component: ComposantWithRelations, + ) { + const { composantModelId, typeComposantId } = component; + if (!composantModelId || !typeComposantId) { + return; + } + + const model = await this.prisma.composantModel.findUnique({ + where: { id: composantModelId }, + select: { structure: true }, + }); + + if (!model?.structure) { + return; + } + + await this.syncComponentStructureCustomFields( + model.structure, + typeComposantId, + ); + } + + private async syncComponentStructureCustomFields( + structure: any, + typeComposantId: string | null, + ) { + if (typeComposantId) { + await this.ensureCustomFieldsForType( + 'typeComposantId', + typeComposantId, + structure?.customFields, + ); + } + + const pieces = Array.isArray(structure?.pieces) ? structure.pieces : []; + for (const piece of pieces) { + const typePieceId = this.extractTypePieceId(piece); + if (typePieceId) { + await this.ensureCustomFieldsForType( + 'typePieceId', + typePieceId, + piece?.customFields, + ); + } + } + + const subComponents = Array.isArray(structure?.subComponents) + ? structure.subComponents + : []; + for (const sub of subComponents) { + const subTypeId = this.extractTypeComposantId(sub); + if (!subTypeId) { + continue; + } + await this.syncComponentStructureCustomFields(sub, subTypeId); + } + } + + private extractTypePieceId(entry: any): string | null { + if (!entry || typeof entry !== 'object') { + return null; + } + return ( + entry.typePieceId || + entry.typePiece?.id || + null + ); + } + + private extractTypeComposantId(entry: any): string | null { + if (!entry || typeof entry !== 'object') { + return null; + } + return ( + entry.typeComposantId || + entry.typeComposant?.id || + null + ); + } + + private async ensureCustomFieldsForType( + typeKey: 'typeComposantId' | 'typePieceId', + typeId: string | null, + fields: any, + ) { + if (!typeId || !Array.isArray(fields)) { + return; + } + + for (const field of fields) { + if (!field || typeof field !== 'object') { + continue; + } + const name = typeof field.name === 'string' ? field.name.trim() : ''; + if (!name) { + continue; + } + const type = typeof field.type === 'string' && field.type.trim() + ? field.type.trim() + : 'text'; + const required = !!field.required; + const options = this.normalizeOptions(field); + + const existing = await this.prisma.customField.findFirst({ + where: { + name, + type, + [typeKey]: typeId, + }, + }); + + if (!existing) { + await this.prisma.customField.create({ + data: { + name, + type, + required, + options, + [typeKey]: typeId, + }, + }); + } + } + } + + private normalizeOptions(field: any): string[] | undefined { + if (Array.isArray(field?.options)) { + const options = field.options + .map((option: any) => + typeof option === 'string' ? option.trim() : '', + ) + .filter((option: string) => option.length > 0); + return options.length ? options : undefined; + } + + if (typeof field?.optionsText === 'string') { + const options = field.optionsText + .split(/\r?\n/) + .map((option: string) => option.trim()) + .filter((option: string) => option.length > 0); + return options.length ? options : undefined; + } + + return undefined; + } } diff --git a/src/machines/machines.service.ts b/src/machines/machines.service.ts index f476331..e9227f3 100644 --- a/src/machines/machines.service.ts +++ b/src/machines/machines.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { ModelCategory } from '@prisma/client'; +import { Prisma, ModelCategory } from '@prisma/client'; import { PrismaService } from '../prisma/prisma.service'; import { CreateMachineDto, @@ -8,6 +8,11 @@ import { MachineComponentSelectionDto, MachinePieceSelectionDto, } from '../shared/dto/machine.dto'; +import { + COMPONENT_WITH_RELATIONS_INCLUDE, + ComposantWithRelations, +} from '../common/constants/component-includes'; +import { buildComponentHierarchy } from '../common/utils/component-tree.util'; const CUSTOM_FIELD_SELECT = { id: true, @@ -17,16 +22,24 @@ const CUSTOM_FIELD_SELECT = { options: true, } as const; -const TYPE_MACHINE_CONFIGURATION_INCLUDE = { +const TYPE_MACHINE_CONFIGURATION_INCLUDE: Prisma.TypeMachineInclude = { customFields: { select: CUSTOM_FIELD_SELECT }, componentRequirements: { include: { - typeComposant: true, + typeComposant: { + include: { + customFields: true, + }, + }, }, }, pieceRequirements: { include: { - typePiece: true, + typePiece: { + include: { + customFields: true, + }, + }, }, }, }; @@ -38,38 +51,7 @@ const MACHINE_DEFAULT_INCLUDE = { }, constructeur: true, composants: { - include: { - typeComposant: true, - composantModel: true, - typeMachineComponentRequirement: { - include: { - typeComposant: true, - }, - }, - sousComposants: true, - customFieldValues: { - include: { - customField: { select: CUSTOM_FIELD_SELECT }, - }, - }, - constructeur: true, - pieces: { - include: { - customFieldValues: { - include: { - customField: { select: CUSTOM_FIELD_SELECT }, - }, - }, - constructeur: true, - pieceModel: true, - typeMachinePieceRequirement: { - include: { - typePiece: true, - }, - }, - }, - }, - }, + include: COMPONENT_WITH_RELATIONS_INCLUDE, }, pieces: { include: { @@ -79,12 +61,22 @@ const MACHINE_DEFAULT_INCLUDE = { }, }, constructeur: true, + typePiece: { + include: { + customFields: true, + }, + }, pieceModel: true, typeMachinePieceRequirement: { include: { - typePiece: true, + typePiece: { + include: { + customFields: true, + }, + }, }, }, + documents: true, }, }, customFieldValues: { @@ -93,12 +85,36 @@ const MACHINE_DEFAULT_INCLUDE = { }, }, documents: true, -}; +} satisfies Prisma.MachineInclude; + +type MachineWithRelations = Prisma.MachineGetPayload<{ + include: typeof MACHINE_DEFAULT_INCLUDE; +}>; @Injectable() export class MachinesService { constructor(private prisma: PrismaService) {} + private hydrateMachine( + machine: MachineWithRelations | null, + ): MachineWithRelations | null { + if (!machine || !Array.isArray(machine.composants)) { + return machine; + } + + const hierarchy = buildComponentHierarchy( + machine.composants as ComposantWithRelations[], + ); + machine.composants = hierarchy as typeof machine.composants; + return machine; + } + + private hydrateMachines( + machines: MachineWithRelations[], + ): MachineWithRelations[] { + return machines.map((machine) => this.hydrateMachine(machine)!); + } + private slugifyName(name: string): string { return name .normalize('NFD') @@ -378,7 +394,7 @@ export class MachinesService { : [] ) as any[]; - return this.prisma.$transaction(async (prisma) => { + const machine = await this.prisma.$transaction(async (prisma) => { const machine = await prisma.machine.create({ data: machineData, include: { @@ -449,6 +465,7 @@ export class MachinesService { prisma, machine.id, typeMachine.customFields, + typeMachine.id, ); } @@ -457,6 +474,8 @@ export class MachinesService { include: MACHINE_DEFAULT_INCLUDE, }); }); + + return this.hydrateMachine(machine); } private cloneStructure(definition: any): any { @@ -896,26 +915,36 @@ export class MachinesService { prisma: any, machineId: string, machineCustomFields: any[], + typeMachineId?: string, ) { for (const customField of machineCustomFields) { if (!customField || !customField.name) continue; - const createdCustomField = await prisma.customField.create({ - data: { - name: customField.name, - type: customField.type, - required: customField.required || false, - options: customField.options || [], - typeMachineId: null, // Ce champ sera lié à la machine individuelle - }, - }); + const existingCustomFieldId = + customField.id ?? customField.customFieldId ?? null; + + let targetCustomFieldId = existingCustomFieldId; + + if (!targetCustomFieldId) { + const createdCustomField = await prisma.customField.create({ + data: { + name: customField.name, + type: customField.type, + required: customField.required || false, + options: customField.options || [], + typeMachineId: typeMachineId ?? null, + }, + }); + + targetCustomFieldId = createdCustomField.id; + } const providedValue = this.extractCustomFieldValue(customField); - if (providedValue !== undefined) { + if (providedValue !== undefined && targetCustomFieldId) { await prisma.customFieldValue.create({ data: { value: providedValue, - customFieldId: createdCustomField.id, + customFieldId: targetCustomFieldId, machineId, }, }); @@ -924,16 +953,20 @@ export class MachinesService { } async findAll() { - return this.prisma.machine.findMany({ + const machines = await this.prisma.machine.findMany({ include: MACHINE_DEFAULT_INCLUDE, }); + + return this.hydrateMachines(machines); } async findOne(id: string) { - return this.prisma.machine.findUnique({ + const machine = await this.prisma.machine.findUnique({ where: { id }, include: MACHINE_DEFAULT_INCLUDE, }); + + return this.hydrateMachine(machine); } async reconfigure(id: string, reconfigureMachineDto: ReconfigureMachineDto) { @@ -983,7 +1016,7 @@ export class MachinesService { : [] ) as any[]; - return this.prisma.$transaction(async (prisma) => { + const updatedMachine = await this.prisma.$transaction(async (prisma) => { await prisma.customFieldValue.deleteMany({ where: { OR: [ @@ -1074,14 +1107,18 @@ export class MachinesService { include: MACHINE_DEFAULT_INCLUDE, }); }); + + return this.hydrateMachine(updatedMachine); } async update(id: string, updateMachineDto: UpdateMachineDto) { - return this.prisma.machine.update({ + const machine = await this.prisma.machine.update({ where: { id }, data: updateMachineDto, include: MACHINE_DEFAULT_INCLUDE, }); + + return this.hydrateMachine(machine); } private async resolveConstructeurId(prisma: any, rawName?: string) { @@ -1200,23 +1237,40 @@ export class MachinesService { }); if (!existingValue) { - // Créer le champ personnalisé pour la machine - const createdCustomField = await this.prisma.customField.create({ - data: { - name: customField.name, - type: customField.type, - required: customField.required || false, - options: customField.options || [], - typeMachineId: null, // Ce champ sera lié à la machine individuelle - }, - }); + const resolvedCustomFieldId = customField.id + ? customField.id + : ( + await this.prisma.customField.findFirst({ + where: { + name: customField.name, + typeMachineId: machine.typeMachineId, + }, + select: { id: true }, + }) + )?.id; + + let targetCustomFieldId = resolvedCustomFieldId; + + if (!targetCustomFieldId) { + const createdCustomField = await this.prisma.customField.create({ + data: { + name: customField.name, + type: customField.type, + required: customField.required || false, + options: customField.options || [], + typeMachineId: machine.typeMachineId, + }, + }); + + targetCustomFieldId = createdCustomField.id; + } const providedValue = this.extractCustomFieldValue(customField); - if (providedValue !== undefined) { + if (providedValue !== undefined && targetCustomFieldId) { await this.prisma.customFieldValue.create({ data: { value: providedValue, - customFieldId: createdCustomField.id, + customFieldId: targetCustomFieldId, machineId, }, }); diff --git a/src/pieces/pieces.service.ts b/src/pieces/pieces.service.ts index 2a3188e..225f832 100644 --- a/src/pieces/pieces.service.ts +++ b/src/pieces/pieces.service.ts @@ -2,6 +2,33 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; import { CreatePieceDto, UpdatePieceDto } from '../shared/dto/piece.dto'; +const PIECE_WITH_RELATIONS_INCLUDE = { + machine: true, + composant: true, + typePiece: { + include: { + customFields: true, + }, + }, + documents: true, + constructeur: true, + pieceModel: true, + typeMachinePieceRequirement: { + include: { + typePiece: { + include: { + customFields: true, + }, + }, + }, + }, + customFieldValues: { + include: { + customField: true, + }, + }, +} as const; + @Injectable() export class PiecesService { constructor(private prisma: PrismaService) {} @@ -146,24 +173,7 @@ export class PiecesService { async findByMachine(machineId: string) { return this.prisma.piece.findMany({ where: { machineId }, - include: { - machine: true, - composant: true, - typePiece: true, - documents: true, - constructeur: true, - pieceModel: true, - typeMachinePieceRequirement: { - include: { - typePiece: true, - }, - }, - customFieldValues: { - include: { - customField: true, - }, - }, - }, + include: PIECE_WITH_RELATIONS_INCLUDE, }); } @@ -199,39 +209,20 @@ export class PiecesService { async findByComposant(composantId: string) { return this.prisma.piece.findMany({ where: { composantId }, - include: { - machine: true, - composant: true, - typePiece: true, - documents: true, - constructeur: true, - pieceModel: true, - typeMachinePieceRequirement: { - include: { - typePiece: true, - }, - }, - customFieldValues: { - include: { - customField: true, - }, - }, - }, + include: PIECE_WITH_RELATIONS_INCLUDE, }); } async update(id: string, updatePieceDto: UpdatePieceDto) { - return this.prisma.piece.update({ + const updated = await this.prisma.piece.update({ where: { id }, data: updatePieceDto, - include: { - machine: true, - composant: true, - typePiece: true, - documents: true, - constructeur: true, - }, + include: PIECE_WITH_RELATIONS_INCLUDE, }); + + await this.syncPieceModelCustomFields(updated); + + return updated; } async remove(id: string) { @@ -239,4 +230,137 @@ export class PiecesService { where: { id }, }); } + + private async syncPieceModelCustomFields(piece: any) { + const pieceModelId = piece?.pieceModelId; + + if (!pieceModelId) { + return; + } + + const model = await this.prisma.pieceModel.findUnique({ + where: { id: pieceModelId }, + select: { structure: true }, + }); + + if (!model?.structure) { + return; + } + + const structure = this.asRecord(model.structure); + const customFields = this.extractCustomFields(structure); + + const targetTypePieceId = this.getTypePieceIdForPiece(piece, structure); + if (!targetTypePieceId) { + return; + } + + await this.ensureCustomFieldsForType( + targetTypePieceId, + customFields, + ); + } + + private async ensureCustomFieldsForType( + typePieceId: string, + fields: any, + ) { + if (!typePieceId || !Array.isArray(fields)) { + return; + } + + for (const field of fields) { + if (!field || typeof field !== 'object') { + continue; + } + + const name = typeof field.name === 'string' ? field.name.trim() : ''; + if (!name) { + continue; + } + + const type = typeof field.type === 'string' && field.type.trim() + ? field.type.trim() + : 'text'; + const required = !!field.required; + const options = this.normalizeOptions(field); + + const existing = await this.prisma.customField.findFirst({ + where: { + name, + type, + typePieceId, + }, + }); + + if (!existing) { + await this.prisma.customField.create({ + data: { + name, + type, + required, + options, + typePieceId, + }, + }); + } + } + } + + private normalizeOptions(field: any): string[] | undefined { + if (Array.isArray(field?.options)) { + const normalized = field.options + .map((option: any) => + typeof option === 'string' ? option.trim() : '', + ) + .filter((option: string) => option.length > 0); + + return normalized.length ? normalized : undefined; + } + + if (typeof field?.optionsText === 'string') { + const normalized = field.optionsText + .split(/\r?\n/) + .map((option: string) => option.trim()) + .filter((option: string) => option.length > 0); + + return normalized.length ? normalized : undefined; + } + + return undefined; + } + + private getTypePieceIdForPiece( + piece: any, + modelStructure: Record | null, + ): string | null { + const structure = this.asRecord(modelStructure); + const structureTypePiece = this.asRecord(structure?.typePiece ?? null); + + return ( + piece?.typePieceId || + piece?.typePiece?.id || + piece?.typeMachinePieceRequirement?.typePieceId || + piece?.typeMachinePieceRequirement?.typePiece?.id || + structure?.typePieceId || + structureTypePiece?.id || + null + ); + } + + private asRecord(value: unknown): Record | null { + if (!value || typeof value !== 'object' || Array.isArray(value)) { + return null; + } + return value as Record; + } + + private extractCustomFields(structure: Record | null): any[] { + if (!structure) { + return []; + } + + const { customFields } = structure; + return Array.isArray(customFields) ? customFields : []; + } }