feat: synchronize backend and frontend custom field handling
This commit is contained in:
64
src/common/constants/component-includes.ts
Normal file
64
src/common/constants/component-includes.ts
Normal 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[];
|
||||
}
|
||||
51
src/common/utils/component-tree.util.ts
Normal file
51
src/common/utils/component-tree.util.ts
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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<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 : [];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user