feat: synchronize backend and frontend custom field handling

This commit is contained in:
Matthieu
2025-09-30 15:35:32 +02:00
parent bd058cd533
commit 5a366595e6
5 changed files with 613 additions and 520 deletions

View File

@@ -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[];
}

View File

@@ -0,0 +1,51 @@
export interface HierarchicalComponent<T = any> {
id: string;
parentComposantId?: string | null;
sousComposants?: T[];
}
export function buildComponentHierarchy<T extends HierarchicalComponent<T>>(
components: readonly T[],
): T[] {
if (!Array.isArray(components) || components.length === 0) {
return [];
}
const byParent = new Map<string | null, T[]>();
components.forEach((raw) => {
const component = raw as HierarchicalComponent<T>;
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<T extends HierarchicalComponent<T>>(
components: T[],
rootId: string,
): T | null {
if (!Array.isArray(components) || components.length === 0) {
return null;
}
const map = new Map<string, T>();
components.forEach((component) => {
map.set(component.id, component);
});
buildComponentHierarchy(components);
return map.get(rootId) ?? null;
}

View File

@@ -4,11 +4,52 @@ import {
CreateComposantDto, CreateComposantDto,
UpdateComposantDto, UpdateComposantDto,
} from '../shared/dto/composant.dto'; } 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() @Injectable()
export class ComposantsService { export class ComposantsService {
constructor(private prisma: PrismaService) {} constructor(private prisma: PrismaService) {}
private async fetchComponentsByMachine(
machineId: string,
): Promise<ComposantWithRelations[]> {
return this.prisma.composant.findMany({
where: { machineId },
include: COMPONENT_WITH_RELATIONS_INCLUDE,
}) as Promise<ComposantWithRelations[]>;
}
private async getComponentWithHierarchy(
id: string,
): Promise<ComposantWithRelations | null> {
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) { async create(createComposantDto: CreateComposantDto) {
const requirementId = createComposantDto.typeMachineComponentRequirementId; const requirementId = createComposantDto.typeMachineComponentRequirementId;
@@ -77,434 +118,46 @@ export class ComposantsService {
createComposantDto.typeComposantId ?? requirement.typeComposantId, createComposantDto.typeComposantId ?? requirement.typeComposantId,
}; };
return this.prisma.composant.create({ const created = (await this.prisma.composant.create({
data, data,
include: { include: COMPONENT_WITH_RELATIONS_INCLUDE,
machine: true, })) as ComposantWithRelations;
parentComposant: true,
typeComposant: true, return this.getComponentWithHierarchy(created.id);
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,
},
});
} }
async findAll() { async findAll() {
return this.prisma.composant.findMany({ const components = (await this.prisma.composant.findMany({
include: { include: COMPONENT_WITH_RELATIONS_INCLUDE,
machine: true, })) as ComposantWithRelations[];
parentComposant: true,
typeComposant: true, return buildComponentHierarchy(components);
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,
},
});
} }
async findOne(id: string) { async findOne(id: string) {
return this.prisma.composant.findUnique({ return this.getComponentWithHierarchy(id);
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,
},
});
} }
async findByMachine(machineId: string) { async findByMachine(machineId: string) {
return this.prisma.composant.findMany({ const components = await this.fetchComponentsByMachine(machineId);
where: { machineId }, return buildComponentHierarchy(components);
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,
},
});
} }
async findHierarchy(machineId: string) { async findHierarchy(machineId: string) {
// Récupérer tous les composants de premier niveau (sans parent) const components = await this.fetchComponentsByMachine(machineId);
const rootComposants = await this.prisma.composant.findMany({ return buildComponentHierarchy(components);
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;
} }
async update(id: string, updateComposantDto: UpdateComposantDto) { async update(id: string, updateComposantDto: UpdateComposantDto) {
return this.prisma.composant.update({ const updated = (await this.prisma.composant.update({
where: { id }, where: { id },
data: updateComposantDto, data: updateComposantDto,
include: { include: COMPONENT_WITH_RELATIONS_INCLUDE,
machine: true, })) as ComposantWithRelations;
parentComposant: true,
typeComposant: true, await this.syncComponentModelCustomFields(updated);
composantModel: true,
typeMachineComponentRequirement: { return this.getComponentWithHierarchy(updated.id);
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,
},
});
} }
private async resolveMachineIdFromComposant( private async resolveMachineIdFromComposant(
@@ -543,4 +196,151 @@ export class ComposantsService {
where: { id }, 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;
}
} }

View File

@@ -1,5 +1,5 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { ModelCategory } from '@prisma/client'; import { Prisma, ModelCategory } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service'; import { PrismaService } from '../prisma/prisma.service';
import { import {
CreateMachineDto, CreateMachineDto,
@@ -8,6 +8,11 @@ import {
MachineComponentSelectionDto, MachineComponentSelectionDto,
MachinePieceSelectionDto, MachinePieceSelectionDto,
} from '../shared/dto/machine.dto'; } 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 = { const CUSTOM_FIELD_SELECT = {
id: true, id: true,
@@ -17,16 +22,24 @@ const CUSTOM_FIELD_SELECT = {
options: true, options: true,
} as const; } as const;
const TYPE_MACHINE_CONFIGURATION_INCLUDE = { const TYPE_MACHINE_CONFIGURATION_INCLUDE: Prisma.TypeMachineInclude = {
customFields: { select: CUSTOM_FIELD_SELECT }, customFields: { select: CUSTOM_FIELD_SELECT },
componentRequirements: { componentRequirements: {
include: { include: {
typeComposant: true, typeComposant: {
include: {
customFields: true,
},
},
}, },
}, },
pieceRequirements: { pieceRequirements: {
include: { include: {
typePiece: true, typePiece: {
include: {
customFields: true,
},
},
}, },
}, },
}; };
@@ -38,38 +51,7 @@ const MACHINE_DEFAULT_INCLUDE = {
}, },
constructeur: true, constructeur: true,
composants: { composants: {
include: { include: COMPONENT_WITH_RELATIONS_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,
},
},
},
},
},
}, },
pieces: { pieces: {
include: { include: {
@@ -79,12 +61,22 @@ const MACHINE_DEFAULT_INCLUDE = {
}, },
}, },
constructeur: true, constructeur: true,
typePiece: {
include: {
customFields: true,
},
},
pieceModel: true, pieceModel: true,
typeMachinePieceRequirement: { typeMachinePieceRequirement: {
include: { include: {
typePiece: true, typePiece: {
include: {
customFields: true,
},
},
}, },
}, },
documents: true,
}, },
}, },
customFieldValues: { customFieldValues: {
@@ -93,12 +85,36 @@ const MACHINE_DEFAULT_INCLUDE = {
}, },
}, },
documents: true, documents: true,
}; } satisfies Prisma.MachineInclude;
type MachineWithRelations = Prisma.MachineGetPayload<{
include: typeof MACHINE_DEFAULT_INCLUDE;
}>;
@Injectable() @Injectable()
export class MachinesService { export class MachinesService {
constructor(private prisma: PrismaService) {} 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 { private slugifyName(name: string): string {
return name return name
.normalize('NFD') .normalize('NFD')
@@ -378,7 +394,7 @@ export class MachinesService {
: [] : []
) as any[]; ) as any[];
return this.prisma.$transaction(async (prisma) => { const machine = await this.prisma.$transaction(async (prisma) => {
const machine = await prisma.machine.create({ const machine = await prisma.machine.create({
data: machineData, data: machineData,
include: { include: {
@@ -449,6 +465,7 @@ export class MachinesService {
prisma, prisma,
machine.id, machine.id,
typeMachine.customFields, typeMachine.customFields,
typeMachine.id,
); );
} }
@@ -457,6 +474,8 @@ export class MachinesService {
include: MACHINE_DEFAULT_INCLUDE, include: MACHINE_DEFAULT_INCLUDE,
}); });
}); });
return this.hydrateMachine(machine);
} }
private cloneStructure(definition: any): any { private cloneStructure(definition: any): any {
@@ -896,26 +915,36 @@ export class MachinesService {
prisma: any, prisma: any,
machineId: string, machineId: string,
machineCustomFields: any[], machineCustomFields: any[],
typeMachineId?: string,
) { ) {
for (const customField of machineCustomFields) { for (const customField of machineCustomFields) {
if (!customField || !customField.name) continue; if (!customField || !customField.name) continue;
const createdCustomField = await prisma.customField.create({ const existingCustomFieldId =
data: { customField.id ?? customField.customFieldId ?? null;
name: customField.name,
type: customField.type, let targetCustomFieldId = existingCustomFieldId;
required: customField.required || false,
options: customField.options || [], if (!targetCustomFieldId) {
typeMachineId: null, // Ce champ sera lié à la machine individuelle 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); const providedValue = this.extractCustomFieldValue(customField);
if (providedValue !== undefined) { if (providedValue !== undefined && targetCustomFieldId) {
await prisma.customFieldValue.create({ await prisma.customFieldValue.create({
data: { data: {
value: providedValue, value: providedValue,
customFieldId: createdCustomField.id, customFieldId: targetCustomFieldId,
machineId, machineId,
}, },
}); });
@@ -924,16 +953,20 @@ export class MachinesService {
} }
async findAll() { async findAll() {
return this.prisma.machine.findMany({ const machines = await this.prisma.machine.findMany({
include: MACHINE_DEFAULT_INCLUDE, include: MACHINE_DEFAULT_INCLUDE,
}); });
return this.hydrateMachines(machines);
} }
async findOne(id: string) { async findOne(id: string) {
return this.prisma.machine.findUnique({ const machine = await this.prisma.machine.findUnique({
where: { id }, where: { id },
include: MACHINE_DEFAULT_INCLUDE, include: MACHINE_DEFAULT_INCLUDE,
}); });
return this.hydrateMachine(machine);
} }
async reconfigure(id: string, reconfigureMachineDto: ReconfigureMachineDto) { async reconfigure(id: string, reconfigureMachineDto: ReconfigureMachineDto) {
@@ -983,7 +1016,7 @@ export class MachinesService {
: [] : []
) as any[]; ) as any[];
return this.prisma.$transaction(async (prisma) => { const updatedMachine = await this.prisma.$transaction(async (prisma) => {
await prisma.customFieldValue.deleteMany({ await prisma.customFieldValue.deleteMany({
where: { where: {
OR: [ OR: [
@@ -1074,14 +1107,18 @@ export class MachinesService {
include: MACHINE_DEFAULT_INCLUDE, include: MACHINE_DEFAULT_INCLUDE,
}); });
}); });
return this.hydrateMachine(updatedMachine);
} }
async update(id: string, updateMachineDto: UpdateMachineDto) { async update(id: string, updateMachineDto: UpdateMachineDto) {
return this.prisma.machine.update({ const machine = await this.prisma.machine.update({
where: { id }, where: { id },
data: updateMachineDto, data: updateMachineDto,
include: MACHINE_DEFAULT_INCLUDE, include: MACHINE_DEFAULT_INCLUDE,
}); });
return this.hydrateMachine(machine);
} }
private async resolveConstructeurId(prisma: any, rawName?: string) { private async resolveConstructeurId(prisma: any, rawName?: string) {
@@ -1200,23 +1237,40 @@ export class MachinesService {
}); });
if (!existingValue) { if (!existingValue) {
// Créer le champ personnalisé pour la machine const resolvedCustomFieldId = customField.id
const createdCustomField = await this.prisma.customField.create({ ? customField.id
data: { : (
name: customField.name, await this.prisma.customField.findFirst({
type: customField.type, where: {
required: customField.required || false, name: customField.name,
options: customField.options || [], typeMachineId: machine.typeMachineId,
typeMachineId: null, // Ce champ sera lié à la machine individuelle },
}, 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); const providedValue = this.extractCustomFieldValue(customField);
if (providedValue !== undefined) { if (providedValue !== undefined && targetCustomFieldId) {
await this.prisma.customFieldValue.create({ await this.prisma.customFieldValue.create({
data: { data: {
value: providedValue, value: providedValue,
customFieldId: createdCustomField.id, customFieldId: targetCustomFieldId,
machineId, machineId,
}, },
}); });

View File

@@ -2,6 +2,33 @@ import { BadRequestException, Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service'; import { PrismaService } from '../prisma/prisma.service';
import { CreatePieceDto, UpdatePieceDto } from '../shared/dto/piece.dto'; 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() @Injectable()
export class PiecesService { export class PiecesService {
constructor(private prisma: PrismaService) {} constructor(private prisma: PrismaService) {}
@@ -146,24 +173,7 @@ export class PiecesService {
async findByMachine(machineId: string) { async findByMachine(machineId: string) {
return this.prisma.piece.findMany({ return this.prisma.piece.findMany({
where: { machineId }, where: { machineId },
include: { include: PIECE_WITH_RELATIONS_INCLUDE,
machine: true,
composant: true,
typePiece: true,
documents: true,
constructeur: true,
pieceModel: true,
typeMachinePieceRequirement: {
include: {
typePiece: true,
},
},
customFieldValues: {
include: {
customField: true,
},
},
},
}); });
} }
@@ -199,39 +209,20 @@ export class PiecesService {
async findByComposant(composantId: string) { async findByComposant(composantId: string) {
return this.prisma.piece.findMany({ return this.prisma.piece.findMany({
where: { composantId }, where: { composantId },
include: { include: PIECE_WITH_RELATIONS_INCLUDE,
machine: true,
composant: true,
typePiece: true,
documents: true,
constructeur: true,
pieceModel: true,
typeMachinePieceRequirement: {
include: {
typePiece: true,
},
},
customFieldValues: {
include: {
customField: true,
},
},
},
}); });
} }
async update(id: string, updatePieceDto: UpdatePieceDto) { async update(id: string, updatePieceDto: UpdatePieceDto) {
return this.prisma.piece.update({ const updated = await this.prisma.piece.update({
where: { id }, where: { id },
data: updatePieceDto, data: updatePieceDto,
include: { include: PIECE_WITH_RELATIONS_INCLUDE,
machine: true,
composant: true,
typePiece: true,
documents: true,
constructeur: true,
},
}); });
await this.syncPieceModelCustomFields(updated);
return updated;
} }
async remove(id: string) { async remove(id: string) {
@@ -239,4 +230,137 @@ export class PiecesService {
where: { id }, 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<string, any> | 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<string, any> | null {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return null;
}
return value as Record<string, any>;
}
private extractCustomFields(structure: Record<string, any> | null): any[] {
if (!structure) {
return [];
}
const { customFields } = structure;
return Array.isArray(customFields) ? customFields : [];
}
} }