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

@@ -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<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) {
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;
}
}