refactor: prepare multi-machine inventory associations

This commit is contained in:
Matthieu
2025-10-08 16:23:49 +02:00
parent c23ba3a587
commit 48a74b74d7
19 changed files with 1166 additions and 297 deletions

View File

@@ -0,0 +1,42 @@
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
async function main () {
try {
console.log('Starting custom fields cleanup...')
const deletedValues = await prisma.customFieldValue.deleteMany({
where: {
customField: {
OR: [
{ typeComposantId: { not: null } },
{ typePieceId: { not: null } },
{ typeMachineId: { not: null } }
]
}
}
})
console.log(`Deleted ${deletedValues.count} custom field values linked to type-level definitions.`)
const deletedFields = await prisma.customField.deleteMany({
where: {
OR: [
{ typeComposantId: { not: null } },
{ typePieceId: { not: null } },
{ typeMachineId: { not: null } }
]
}
})
console.log(`Deleted ${deletedFields.count} custom field definitions linked to model types.`)
console.log('Cleanup complete.')
} catch (error) {
console.error('Cleanup failed:', error)
process.exitCode = 1
} finally {
await prisma.$disconnect()
}
}
main()

View File

@@ -0,0 +1,593 @@
import { Prisma, PrismaClient, ModelCategory } from '@prisma/client';
const prisma = new PrismaClient();
type CustomFieldInput = {
name: string;
type: 'text' | 'number' | 'select';
required?: boolean;
options?: readonly string[];
};
type ModelTypeSeed = {
code: string;
name: string;
description: string;
customFields: readonly CustomFieldInput[];
};
type ComponentRequirementSeed = {
typeCode: string;
label: string;
minCount: number;
maxCount?: number | null;
required?: boolean;
allowNewModels?: boolean;
};
type PieceRequirementSeed = {
typeCode: string;
label: string;
minCount: number;
maxCount?: number | null;
required?: boolean;
allowNewModels?: boolean;
};
const componentTypes: readonly ModelTypeSeed[] = [
{
code: 'drive-module',
name: 'Module d entrainement',
description: 'Sous-ensemble moteur et reducteur pour entrainements principaux.',
customFields: [
{ name: 'Puissance nominale (kW)', type: 'number', required: true },
{ name: 'Indice de protection', type: 'select', options: ['IP55', 'IP65', 'IP66'] },
],
},
{
code: 'sensor-array',
name: 'Chaine de capteurs',
description: 'Groupe de capteurs industriels (temperature, vibration, debit).',
customFields: [
{ name: 'Type principal', type: 'select', options: ['Temperature', 'Vibration', 'Debit'] },
{ name: 'Plage de mesure', type: 'text' },
],
},
{
code: 'control-cabinet',
name: 'Armoire de controle',
description: 'Armoire electrique avec automate, protection et distribution.',
customFields: [
{ name: 'Tension alimentation (V)', type: 'number' },
{ name: 'Nombre de departs', type: 'number' },
],
},
{
code: 'hydraulic-pack',
name: 'Groupe hydraulique',
description: 'Bloc hydraulique complet (pompes, accumulateurs, filtration).',
customFields: [
{ name: 'Pression nominale (bar)', type: 'number', required: true },
{ name: 'Debit nominal (L/min)', type: 'number' },
],
},
{
code: 'structure-frame',
name: 'Chassis structurel',
description: 'Structure porteuse ou chassis mecano-soude.',
customFields: [
{ name: 'Matiere', type: 'select', options: ['Acier', 'Inox', 'Aluminium'] },
{ name: 'Charge admissible (kg)', type: 'number' },
],
},
];
const pieceTypes: readonly ModelTypeSeed[] = [
{
code: 'belt-kit',
name: 'Kit courroie',
description: 'Courroie et accessoires pour entrainements.',
customFields: [
{ name: 'Type', type: 'select', options: ['Poly-V', 'Trapezoidale', 'Synchronisee'] },
{ name: 'Longueur (mm)', type: 'number' },
],
},
{
code: 'bearing-set',
name: 'Jeu de roulements',
description: 'Paire de roulements avec bagues et graisse.',
customFields: [
{ name: 'Diametre interieur (mm)', type: 'number', required: true },
{ name: 'Classe', type: 'select', options: ['P0', 'P6', 'P5'] },
],
},
{
code: 'filter-cartridge',
name: 'Cartouche filtrante',
description: 'Element filtrant pour fluides ou air.',
customFields: [
{ name: 'Grade de filtration (um)', type: 'number' },
{ name: 'Type de media', type: 'select', options: ['Cellulose', 'Synthetique', 'Inox'] },
],
},
{
code: 'sensor-probe',
name: 'Sonde de mesure',
description: 'Sonde ou capteur unitaire avec cable.',
customFields: [
{ name: 'Signal de sortie', type: 'select', options: ['4-20 mA', '0-10 V', 'PT100'] },
{ name: 'Indice IP', type: 'select', options: ['IP67', 'IP68'] },
],
},
{
code: 'maintenance-kit',
name: 'Kit maintenance',
description: 'Ensemble de pieces pour maintenance planifiee.',
customFields: [
{ name: 'Niveau de maintenance', type: 'select', options: ['Preventif', 'Correctif', 'Lourde'] },
{ name: 'Duree estimee (h)', type: 'number' },
],
},
];
const constructors = [
{ name: 'ElectroMec Industrie', email: 'contact@electromec.fr', phone: '+33 4 72 00 11 22' },
{ name: 'Hydraulic Systems Europe', email: 'sales@hydraulics-eu.com', phone: '+33 5 56 12 34 56' },
{ name: 'Automation Lyon', email: 'support@automation-lyon.fr', phone: '+33 4 37 50 60 70' },
{ name: 'ThermoTech Solutions', email: 'info@thermotech.eu', phone: '+33 1 44 55 66 77' },
{ name: 'BearingWorks', email: 'service@bearingworks.com', phone: '+33 3 88 90 12 45' },
] as const;
const machineCustomFields: readonly CustomFieldInput[] = [
{ name: 'Reference installation', type: 'text' },
{ name: 'Puissance installee (kW)', type: 'number' },
{ name: 'Zone critique', type: 'select', options: ['Zone A', 'Zone B', 'Zone C'] },
];
const componentRequirementSeeds: readonly ComponentRequirementSeed[] = [
{
typeCode: 'structure-frame',
label: 'Chassis principal',
minCount: 1,
maxCount: 1,
required: true,
allowNewModels: false,
},
{
typeCode: 'drive-module',
label: 'Module d entrainement principal',
minCount: 1,
maxCount: 1,
required: true,
allowNewModels: false,
},
{
typeCode: 'control-cabinet',
label: 'Armoire de controle',
minCount: 1,
maxCount: 1,
required: true,
allowNewModels: false,
},
{
typeCode: 'sensor-array',
label: 'Capteurs de surveillance',
minCount: 1,
maxCount: 3,
required: true,
allowNewModels: true,
},
{
typeCode: 'hydraulic-pack',
label: 'Groupe hydraulique auxiliaire',
minCount: 0,
maxCount: 1,
required: false,
allowNewModels: true,
},
];
const pieceRequirementSeeds: readonly PieceRequirementSeed[] = [
{
typeCode: 'belt-kit',
label: 'Kit courroie de rechange',
minCount: 1,
maxCount: 2,
required: true,
allowNewModels: true,
},
{
typeCode: 'bearing-set',
label: 'Roulements de secours',
minCount: 1,
maxCount: 2,
required: true,
allowNewModels: true,
},
{
typeCode: 'filter-cartridge',
label: 'Cartouches de filtration',
minCount: 0,
maxCount: 4,
required: false,
allowNewModels: true,
},
{
typeCode: 'maintenance-kit',
label: 'Kit maintenance planifiee',
minCount: 0,
maxCount: 1,
required: false,
allowNewModels: true,
},
{
typeCode: 'sensor-probe',
label: 'Sondes de rechange',
minCount: 1,
maxCount: 4,
required: true,
allowNewModels: true,
},
];
function mapCustomFields(fields: readonly CustomFieldInput[]) {
if (!fields.length) {
return undefined;
}
return {
create: fields.map((field) => ({
name: field.name,
type: field.type,
required: field.required ?? false,
options: field.options ? [...field.options] : [],
})),
} as const;
}
async function upsertComponentType(type: ModelTypeSeed) {
const customFields = mapCustomFields(type.customFields);
await prisma.modelType.upsert({
where: { code: type.code },
update: {
name: type.name,
description: type.description,
notes: type.description,
customFields: {
deleteMany: {},
...(customFields ?? {}),
},
},
create: {
code: type.code,
name: type.name,
description: type.description,
notes: type.description,
category: ModelCategory.COMPONENT,
...(customFields ? { customFields } : {}),
},
});
}
async function upsertPieceType(type: ModelTypeSeed) {
const customFields = mapCustomFields(type.customFields);
await prisma.modelType.upsert({
where: { code: type.code },
update: {
name: type.name,
description: type.description,
notes: type.description,
pieceCustomFields: {
deleteMany: {},
...(customFields ?? {}),
},
},
create: {
code: type.code,
name: type.name,
description: type.description,
notes: type.description,
category: ModelCategory.PIECE,
...(customFields ? { pieceCustomFields: customFields } : {}),
},
});
}
async function applyPieceSkeletons(pieceMap: Map<string, { id: string }>) {
type PieceSkeleton = {
customFields?: Array<{ name: string; value?: unknown; type?: string; required?: boolean; options?: unknown }>;
[key: string]: unknown;
};
const definitions: Record<string, PieceSkeleton> = {
'belt-kit': {
customFields: [
{ name: 'Type', value: 'Poly-V' },
{ name: 'Longueur (mm)', value: 1800 },
],
remplacementHeures: 1500,
stockageRecommande: 'Local sec et tempere',
},
'bearing-set': {
customFields: [
{ name: 'Diametre interieur (mm)', value: 45 },
{ name: 'Classe', value: 'P6' },
],
graisseRecommandee: 'Lithium NLGI2',
},
'filter-cartridge': {
customFields: [
{ name: 'Grade de filtration (um)', value: 10 },
{ name: 'Type de media', value: 'Synthetique' },
],
remplacementMensuel: true,
},
'sensor-probe': {
customFields: [
{ name: 'Signal de sortie', value: '4-20 mA' },
{ name: 'Indice IP', value: 'IP67' },
],
calibrationIntervalJours: 180,
},
'maintenance-kit': {
customFields: [
{ name: 'Niveau de maintenance', value: 'Preventif' },
{ name: 'Duree estimee (h)', value: 4 },
],
contenuStandard: ['Filtres', 'Joints', 'Visserie'],
},
};
for (const [code, structure] of Object.entries(definitions)) {
const record = pieceMap.get(code);
if (!record) {
continue;
}
await prisma.modelType.update({
where: { id: record.id },
data: {
pieceSkeleton: structure as Prisma.InputJsonValue,
},
});
}
}
async function applyComponentSkeletons(
componentMap: Map<string, { id: string }>,
pieceMap: Map<string, { id: string }>,
) {
const pieceRef = (code: string, role?: string) => {
const piece = pieceMap.get(code);
if (!piece) {
throw new Error(`Piece type ${code} requis pour le squelette`);
}
return {
typePieceId: piece.id,
...(role ? { role } : {}),
};
};
const componentRef = (code: string, alias?: string) => {
const component = componentMap.get(code);
if (!component) {
throw new Error(`Component type ${code} requis pour le squelette`);
}
return {
typeComposantId: component.id,
...(alias ? { alias } : {}),
};
};
type ComponentSkeleton = {
pieces: Array<{ typePieceId: string; role?: string }>;
customFields: Array<{ key: string; value: unknown }>;
subcomponents: Array<{ typeComposantId?: string; alias?: string; familyCode?: string; modelId?: string }>;
};
const definitions: Record<string, ComponentSkeleton> = {
'drive-module': {
pieces: [
pieceRef('belt-kit', 'Courroie principale'),
pieceRef('bearing-set', 'Roulements de sortie'),
],
customFields: [
{ key: 'Lubrification', value: 'Graissage centralise' },
{ key: 'ControleVibration', value: 'Capteurs integres' },
],
subcomponents: [componentRef('sensor-array', 'Capteurs integres')],
},
'sensor-array': {
pieces: [pieceRef('sensor-probe', 'Sonde principale')],
customFields: [
{ key: 'Calibration', value: 'A effectuer tous les 6 mois' },
{ key: 'NombreCapteursMax', value: 6 },
],
subcomponents: [],
},
'control-cabinet': {
pieces: [
pieceRef('maintenance-kit', 'Kit rechange armoire'),
pieceRef('sensor-probe', 'Sonde ambiance'),
],
customFields: [
{ key: 'ClassementLocal', value: 'Non ATEX' },
{ key: 'RefAutomate', value: 'PLC-STD-200' },
],
subcomponents: [],
},
'hydraulic-pack': {
pieces: [
pieceRef('filter-cartridge', 'Filtre hydraulique'),
pieceRef('maintenance-kit', 'Kit joints hydrauliques'),
],
customFields: [
{ key: 'ReservoirLitres', value: 120 },
{ key: 'TypeHuile', value: 'HLP46' },
],
subcomponents: [componentRef('sensor-array', 'Capteurs pression et debit')],
},
'structure-frame': {
pieces: [],
customFields: [
{ key: 'Revêtement', value: 'Peinture epoxy' },
{ key: 'PointsLevage', value: 4 },
],
subcomponents: [componentRef('sensor-array', 'Capteurs deformation')],
},
};
for (const [code, structure] of Object.entries(definitions)) {
const record = componentMap.get(code);
if (!record) {
continue;
}
await prisma.modelType.update({
where: { id: record.id },
data: {
componentSkeleton: structure as Prisma.InputJsonValue,
},
});
}
}
function buildComponentRequirements(
componentMap: Map<string, { id: string }>,
seeds: readonly ComponentRequirementSeed[],
) {
return seeds.map((seed) => {
const type = componentMap.get(seed.typeCode);
if (!type) {
throw new Error(`Type composant ${seed.typeCode} introuvable pour le requirement`);
}
return {
label: seed.label,
minCount: seed.minCount,
maxCount: seed.maxCount ?? null,
required: seed.required ?? true,
allowNewModels: seed.allowNewModels ?? true,
typeComposant: { connect: { id: type.id } },
};
});
}
function buildPieceRequirements(
pieceMap: Map<string, { id: string }>,
seeds: readonly PieceRequirementSeed[],
) {
return seeds.map((seed) => {
const type = pieceMap.get(seed.typeCode);
if (!type) {
throw new Error(`Type piece ${seed.typeCode} introuvable pour le requirement`);
}
return {
label: seed.label,
minCount: seed.minCount,
maxCount: seed.maxCount ?? null,
required: seed.required ?? true,
allowNewModels: seed.allowNewModels ?? true,
typePiece: { connect: { id: type.id } },
};
});
}
async function seedMachineTemplate(
componentMap: Map<string, { id: string }>,
pieceMap: Map<string, { id: string }>,
) {
const name = 'Cellule Modulaire Standard';
const description = 'Module generique compose d un chassis, d un entrainement, de capteurs et d une armoire de controle.';
const componentRequirements = buildComponentRequirements(componentMap, componentRequirementSeeds);
const pieceRequirements = buildPieceRequirements(pieceMap, pieceRequirementSeeds);
await prisma.typeMachine.upsert({
where: { name },
update: {
description,
category: 'Module',
maintenanceFrequency: 'Mensuelle',
customFields: {
deleteMany: {},
...(mapCustomFields(machineCustomFields) ?? {}),
},
componentRequirements: {
deleteMany: {},
create: componentRequirements,
},
pieceRequirements: {
deleteMany: {},
create: pieceRequirements,
},
},
create: {
name,
description,
category: 'Module',
maintenanceFrequency: 'Mensuelle',
...(mapCustomFields(machineCustomFields) ? { customFields: mapCustomFields(machineCustomFields)! } : {}),
componentRequirements: {
create: componentRequirements,
},
pieceRequirements: {
create: pieceRequirements,
},
},
});
}
async function main() {
console.log('Seeding component categories...');
for (const component of componentTypes) {
await upsertComponentType(component);
}
console.log('Seeding piece categories...');
for (const piece of pieceTypes) {
await upsertPieceType(piece);
}
const componentRecords = await prisma.modelType.findMany({
where: { code: { in: componentTypes.map((type) => type.code) } },
select: { id: true, code: true },
});
const pieceRecords = await prisma.modelType.findMany({
where: { code: { in: pieceTypes.map((type) => type.code) } },
select: { id: true, code: true },
});
const componentMap = new Map(componentRecords.map((record) => [record.code, { id: record.id }]));
const pieceMap = new Map(pieceRecords.map((record) => [record.code, { id: record.id }]));
console.log('Applying piece skeletons...');
await applyPieceSkeletons(pieceMap);
console.log('Applying component skeletons...');
await applyComponentSkeletons(componentMap, pieceMap);
console.log('Seeding constructors...');
for (const constructeur of constructors) {
await prisma.constructeur.upsert({
where: { name: constructeur.name },
update: {
email: constructeur.email,
phone: constructeur.phone,
},
create: constructeur,
});
}
console.log('Configuring machine template...');
await seedMachineTemplate(componentMap, pieceMap);
}
main()
.then(() => {
console.log('Seed completed.');
})
.catch((error) => {
console.error('Seed failed:', error);
process.exitCode = 1;
})
.finally(async () => {
await prisma.$disconnect();
});

View File

@@ -50,7 +50,9 @@ export class ModelTypeMapper {
})),
}
: undefined,
...(skeleton ? { componentSkeleton: skeleton as Prisma.InputJsonValue } : {}),
...(skeleton
? { componentSkeleton: skeleton as Prisma.InputJsonValue }
: {}),
};
}

View File

@@ -53,28 +53,38 @@ export function normalizeComponentModelStructure(
return {
familyCode:
ensureString(
candidate?.familyCode ?? candidate?.name ?? candidate?.typePieceLabel ?? 'UNKNOWN',
candidate?.familyCode ??
candidate?.name ??
candidate?.typePieceLabel ??
'UNKNOWN',
).trim() || 'UNKNOWN',
role: sanitizeRole(candidate?.role),
} as ComponentModelStructure['pieces'][number];
});
const customFields = toArray((structure as any)?.customFields).map((field) => {
const candidate = field as Record<string, unknown> | null | undefined;
const key = ensureString(candidate?.key ?? candidate?.name ?? 'unknown').trim();
const customFields = toArray((structure as any)?.customFields).map(
(field) => {
const candidate = field as Record<string, unknown> | null | undefined;
const key = ensureString(
candidate?.key ?? candidate?.name ?? 'unknown',
).trim();
return {
key: key || 'unknown',
value: candidate?.value ?? null,
};
});
return {
key: key || 'unknown',
value: candidate?.value ?? null,
};
},
);
const rawSubcomponents = toArray(
(structure as any)?.subcomponents ?? (structure as any)?.subComponents,
);
const subcomponents = rawSubcomponents.map((subcomponent) => {
const candidate = subcomponent as Record<string, unknown> | null | undefined;
const candidate = subcomponent as
| Record<string, unknown>
| null
| undefined;
if (candidate?.modelId) {
return {
@@ -90,13 +100,15 @@ export function normalizeComponentModelStructure(
}
if (candidate?.typeComposantId) {
return {
typeComposantId: ensureString(candidate.typeComposantId).trim() || 'UNKNOWN',
typeComposantId:
ensureString(candidate.typeComposantId).trim() || 'UNKNOWN',
alias: sanitizeAlias(candidate?.alias ?? candidate?.name),
} as ComponentModelStructure['subcomponents'][number];
}
return {
familyCode: ensureString(candidate?.name ?? 'UNKNOWN').trim() || 'UNKNOWN',
familyCode:
ensureString(candidate?.name ?? 'UNKNOWN').trim() || 'UNKNOWN',
alias: sanitizeAlias(candidate?.alias ?? candidate?.name),
} as ComponentModelStructure['subcomponents'][number];
});

View File

@@ -97,7 +97,9 @@ describe('ComposantsService', () => {
});
prisma.composant.findMany.mockResolvedValue([]);
await expect(service.create(dto)).resolves.toMatchObject({ id: 'component-1' });
await expect(service.create(dto)).resolves.toMatchObject({
id: 'component-1',
});
expect(prisma.composant.create).toHaveBeenCalled();
expect(prisma.composant.create.mock.calls[0][0].data.typeComposantId).toBe(
@@ -193,7 +195,9 @@ describe('ComposantsService', () => {
},
});
prisma.customField.findMany.mockResolvedValue([{ id: 'cf-color', name: 'color' }]);
prisma.customField.findMany.mockResolvedValue([
{ id: 'cf-color', name: 'color' },
]);
prisma.customFieldValue.findMany.mockResolvedValue([]);
const rootComponent = {

View File

@@ -20,10 +20,9 @@ type ComponentRequirementWithType =
Prisma.TypeMachineComponentRequirementGetPayload<{
include: { typeComposant: true };
}>;
type PieceRequirementWithType =
Prisma.TypeMachinePieceRequirementGetPayload<{
include: { typePiece: true };
}>;
type PieceRequirementWithType = Prisma.TypeMachinePieceRequirementGetPayload<{
include: { typePiece: true };
}>;
type ModelTypeWithSkeleton = ComponentRequirementWithType['typeComposant'];
type PieceTypeWithSkeleton = PieceRequirementWithType['typePiece'];
@@ -65,112 +64,137 @@ export class ComposantsService {
}
async create(createComposantDto: CreateComposantDto) {
const requirementId = createComposantDto.typeMachineComponentRequirementId;
const requirementId =
createComposantDto.typeMachineComponentRequirementId ?? null;
let machineId = createComposantDto.machineId;
if (requirementId && !createComposantDto.machineId) {
throw new BadRequestException(
'Un requirement ne peut pas être utilisé sans machine ciblée.',
);
}
let machineId = createComposantDto.machineId ?? null;
if (createComposantDto.parentComposantId) {
const parentMachineId = await this.resolveMachineIdFromComposant(
createComposantDto.parentComposantId,
);
if (machineId && machineId !== parentMachineId) {
if (machineId && parentMachineId && machineId !== parentMachineId) {
throw new BadRequestException(
'Le composant parent ne correspond pas à la machine ciblée.',
);
}
machineId = parentMachineId;
machineId = parentMachineId ?? machineId;
}
if (!machineId) {
throw new BadRequestException(
'Un machineId ou un parentComposantId valide est requis pour créer un composant.',
);
}
let requirement: ComponentRequirementWithType | null = null;
let componentRequirements: ComponentRequirementWithType[] = [];
let pieceRequirements: PieceRequirementWithType[] = [];
const machine = await this.prisma.machine.findUnique({
where: { id: machineId },
include: {
typeMachine: {
include: {
componentRequirements: {
include: {
typeComposant: true,
if (machineId) {
const machine = await this.prisma.machine.findUnique({
where: { id: machineId },
include: {
typeMachine: {
include: {
componentRequirements: {
include: {
typeComposant: true,
},
},
},
pieceRequirements: {
include: {
typePiece: true,
pieceRequirements: {
include: {
typePiece: true,
},
},
},
},
},
},
});
});
if (!machine || !machine.typeMachine) {
throw new BadRequestException(
'La machine ciblée doit être associée à un type de machine pour valider les requirements.',
);
}
if (!machine || !machine.typeMachine) {
throw new BadRequestException(
'La machine ciblée doit être associée à un type de machine pour valider les requirements.',
);
}
const componentRequirements =
(machine.typeMachine.componentRequirements as ComponentRequirementWithType[]) ?? [];
const pieceRequirements =
(machine.typeMachine.pieceRequirements as PieceRequirementWithType[]) ?? [];
componentRequirements =
(machine.typeMachine
.componentRequirements as ComponentRequirementWithType[]) ?? [];
pieceRequirements =
(machine.typeMachine.pieceRequirements as PieceRequirementWithType[]) ??
[];
const requirement = componentRequirements.find(
(componentRequirement) => componentRequirement.id === requirementId,
);
if (requirementId) {
requirement =
componentRequirements.find(
(componentRequirement) => componentRequirement.id === requirementId,
) ?? null;
if (!requirement) {
throw new BadRequestException(
'Le requirement de composant fourni ne correspond pas au squelette de la machine.',
);
}
if (!requirement) {
throw new BadRequestException(
'Le requirement de composant fourni ne correspond pas au squelette de la machine.',
);
}
if (
createComposantDto.typeComposantId &&
createComposantDto.typeComposantId !== requirement.typeComposantId
) {
throw new BadRequestException(
'Le type de composant fourni ne correspond pas au requirement pour cette machine.',
);
if (
createComposantDto.typeComposantId &&
createComposantDto.typeComposantId !== requirement.typeComposantId
) {
throw new BadRequestException(
'Le type de composant fourni ne correspond pas au requirement pour cette machine.',
);
}
}
}
const typeComposantId =
createComposantDto.typeComposantId ?? requirement.typeComposantId;
createComposantDto.typeComposantId ??
requirement?.typeComposantId ??
null;
const created = await this.prisma.composant.create({
data: {
...createComposantDto,
machineId,
typeComposantId,
},
include: COMPONENT_WITH_RELATIONS_INCLUDE,
});
const componentRequirementUsage = new Map<string, number>();
componentRequirementUsage.set(requirement.id, 1);
const pieceRequirementUsage = new Map<string, number>();
await this.populateComponentFromSkeleton({
componentId: created.id,
componentName: created.name,
componentType:
(requirement.typeComposant as ModelTypeWithSkeleton | null) ??
(created.typeComposant as ModelTypeWithSkeleton | null) ??
null,
const data: Prisma.ComposantUncheckedCreateInput = {
name: createComposantDto.name,
reference: createComposantDto.reference ?? null,
constructeurId: createComposantDto.constructeurId ?? null,
prix:
createComposantDto.prix !== undefined ? createComposantDto.prix : null,
machineId,
componentRequirements,
pieceRequirements,
componentRequirementUsage,
pieceRequirementUsage,
});
parentComposantId: createComposantDto.parentComposantId ?? null,
typeComposantId,
typeMachineComponentRequirementId:
requirement?.id ?? requirementId ?? null,
};
const created = (await this.prisma.composant.create({
data,
include: COMPONENT_WITH_RELATIONS_INCLUDE,
})) as ComposantWithRelations;
if (machineId && requirement?.id) {
const componentRequirementUsage = new Map<string, number>();
componentRequirementUsage.set(requirement.id, 1);
const pieceRequirementUsage = new Map<string, number>();
await this.populateComponentFromSkeleton({
componentId: created.id,
componentName: created.name,
componentType:
(requirement.typeComposant as ModelTypeWithSkeleton | null) ??
(created.typeComposant as ModelTypeWithSkeleton | null) ??
null,
machineId,
componentRequirements,
pieceRequirements,
componentRequirementUsage,
pieceRequirementUsage,
});
}
const component = await this.getComponentWithHierarchy(created.id);
return (component as ComposantWithRelations | null) ?? (created as ComposantWithRelations);
return component ?? created;
}
async findAll() {
@@ -225,8 +249,8 @@ export class ComposantsService {
pieceRequirementUsage: Map<string, number>;
}) {
const skeleton = this.parseComponentSkeleton(
(componentType as { componentSkeleton?: Prisma.JsonValue | null } | null)?.
componentSkeleton,
(componentType as { componentSkeleton?: Prisma.JsonValue | null } | null)
?.componentSkeleton,
);
if (!skeleton) {
return;
@@ -274,15 +298,12 @@ export class ComposantsService {
},
});
this.incrementRequirementUsage(
componentRequirementUsage,
requirement.id,
);
this.incrementRequirementUsage(componentRequirementUsage, requirement.id);
await this.populateComponentFromSkeleton({
componentId: createdChild.id,
componentName: createdChild.name,
componentType: requirement.typeComposant as ModelTypeWithSkeleton,
componentType: requirement.typeComposant,
machineId,
componentRequirements,
pieceRequirements,
@@ -311,7 +332,11 @@ export class ComposantsService {
typeComposantId: string | null,
customFields: ComponentModelStructure['customFields'],
) {
if (!typeComposantId || !Array.isArray(customFields) || customFields.length === 0) {
if (
!typeComposantId ||
!Array.isArray(customFields) ||
customFields.length === 0
) {
return;
}
@@ -324,12 +349,16 @@ export class ComposantsService {
return;
}
const definitionMap = new Map(definitions.map((field) => [field.name, field.id]));
const definitionMap = new Map(
definitions.map((field) => [field.name, field.id]),
);
const existingValues = await this.prisma.customFieldValue.findMany({
where: { composantId: componentId },
select: { customFieldId: true },
});
const existingIds = new Set(existingValues.map((value) => value.customFieldId));
const existingIds = new Set(
existingValues.map((value) => value.customFieldId),
);
for (const field of customFields) {
const key = this.normalizeIdentifier(field?.key);
@@ -384,7 +413,11 @@ export class ComposantsService {
continue;
}
const name = this.buildPieceName(entry, requirement.typePiece, componentName);
const name = this.buildPieceName(
entry,
requirement.typePiece,
componentName,
);
await this.prisma.piece.create({
data: {
@@ -418,7 +451,9 @@ export class ComposantsService {
}
if (familyCode && requirement.typeComposant?.code) {
return this.normalizeCode(requirement.typeComposant.code) === familyCode;
return (
this.normalizeCode(requirement.typeComposant.code) === familyCode
);
}
return false;
@@ -516,7 +551,9 @@ export class ComposantsService {
typeComposant: ModelTypeWithSkeleton | null,
parentName?: string,
): string {
const alias = this.normalizeIdentifier((subcomponent as { alias?: string }).alias);
const alias = this.normalizeIdentifier(
(subcomponent as { alias?: string }).alias,
);
if (alias) {
return alias;
}

View File

@@ -170,9 +170,7 @@ export class CustomFieldsService {
}
// Créer ou mettre à jour une valeur de champ personnalisé
async upsertCustomFieldValue(
dto: UpsertCustomFieldValueDto,
) {
async upsertCustomFieldValue(dto: UpsertCustomFieldValueDto) {
const {
customFieldId: rawCustomFieldId,
customFieldName,

View File

@@ -9,7 +9,9 @@ describe('MachinesController', () => {
let controller: MachinesController;
beforeEach(async () => {
const mockComposantsService = { create: jest.fn() } as Partial<ComposantsService>;
const mockComposantsService = {
create: jest.fn(),
} as Partial<ComposantsService>;
const mockPiecesService = { create: jest.fn() } as Partial<PiecesService>;
const module: TestingModule = await Test.createTestingModule({

View File

@@ -8,7 +8,9 @@ describe('MachinesService', () => {
let service: MachinesService;
beforeEach(async () => {
const mockComposantsService = { create: jest.fn() } as Partial<ComposantsService>;
const mockComposantsService = {
create: jest.fn(),
} as Partial<ComposantsService>;
const mockPiecesService = { create: jest.fn() } as Partial<PiecesService>;
const module: TestingModule = await Test.createTestingModule({

View File

@@ -15,8 +15,6 @@ import {
import { buildComponentHierarchy } from '../common/utils/component-tree.util';
import { ComposantsService } from '../composants/composants.service';
import { PiecesService } from '../pieces/pieces.service';
import { CreateComposantDto } from '../shared/dto/composant.dto';
import { CreatePieceDto } from '../shared/dto/piece.dto';
const CUSTOM_FIELD_SELECT = {
id: true,
@@ -103,10 +101,9 @@ type ComponentRequirementWithType =
include: { typeComposant: true };
}>;
type PieceRequirementWithType =
Prisma.TypeMachinePieceRequirementGetPayload<{
include: { typePiece: true };
}>;
type PieceRequirementWithType = Prisma.TypeMachinePieceRequirementGetPayload<{
include: { typePiece: true };
}>;
@Injectable()
export class MachinesService {
@@ -238,7 +235,10 @@ export class MachinesService {
);
}
if (selection.typePieceId && selection.typePieceId !== requirement.typePieceId) {
if (
selection.typePieceId &&
selection.typePieceId !== requirement.typePieceId
) {
throw new Error(
`Le type de pièce sélectionné ne correspond pas au requirement ${requirement.id}.`,
);
@@ -258,7 +258,9 @@ export class MachinesService {
if (selections.length < min) {
throw new Error(
`Le groupe de composants "${
requirement.label || requirement.typeComposant?.name || requirement.id
requirement.label ||
requirement.typeComposant?.name ||
requirement.id
}" requiert au moins ${min} sélection(s).`,
);
}
@@ -266,7 +268,9 @@ export class MachinesService {
if (max !== undefined && selections.length > max) {
throw new Error(
`Le groupe de composants "${
requirement.label || requirement.typeComposant?.name || requirement.id
requirement.label ||
requirement.typeComposant?.name ||
requirement.id
}" ne peut pas dépasser ${max} sélection(s).`,
);
}
@@ -404,86 +408,82 @@ export class MachinesService {
return undefined;
}
private async buildComponentCreationDto(
private async attachExistingComponentToMachine(
machineId: string,
requirement: ComponentRequirementWithType,
selection: MachineComponentSelectionDto,
): Promise<CreateComposantDto> {
const definition = this.ensurePlainObject(selection.definition);
const dto: CreateComposantDto = {
name: this.resolveName(
definition.name,
requirement.label,
requirement.typeComposant?.name,
'Composant',
),
machineId,
typeMachineComponentRequirementId: requirement.id,
};
if (selection.typeComposantId) {
dto.typeComposantId = selection.typeComposantId;
) {
const componentId = selection.composantId;
if (!componentId) {
throw new Error('composantId manquant pour la sélection.');
}
const reference = this.extractString(definition.reference);
if (reference) {
dto.reference = reference;
const component = await this.prisma.composant.findUnique({
where: { id: componentId },
include: { typeComposant: true },
});
if (!component) {
throw new Error(`Composant introuvable (${componentId}).`);
}
const constructeurId = await this.resolveConstructeurId(
definition.constructeurId ?? definition.constructeur,
);
if (constructeurId) {
dto.constructeurId = constructeurId;
if (
requirement.typeComposantId &&
component.typeComposantId &&
component.typeComposantId !== requirement.typeComposantId
) {
throw new Error(
`Le composant sélectionné (${component.name || component.id}) n'appartient pas à la famille attendue pour ce requirement.`,
);
}
const prix = this.normalizePrice(definition.prix);
if (prix !== undefined) {
dto.prix = prix;
}
return dto;
await this.prisma.composant.update({
where: { id: component.id },
data: {
machineId,
parentComposantId: null,
typeMachineComponentRequirementId: requirement.id,
},
});
}
private async buildPieceCreationDto(
private async attachExistingPieceToMachine(
machineId: string,
requirement: PieceRequirementWithType,
selection: MachinePieceSelectionDto,
): Promise<CreatePieceDto> {
const definition = this.ensurePlainObject(selection.definition);
const dto: CreatePieceDto = {
name: this.resolveName(
definition.name,
requirement.label,
requirement.typePiece?.name,
'Pièce',
),
machineId,
typeMachinePieceRequirementId: requirement.id,
};
if (selection.typePieceId) {
dto.typePieceId = selection.typePieceId;
) {
const pieceId = selection.pieceId;
if (!pieceId) {
throw new Error('pieceId manquant pour la sélection.');
}
const reference = this.extractString(definition.reference);
if (reference) {
dto.reference = reference;
const piece = await this.prisma.piece.findUnique({
where: { id: pieceId },
include: { typePiece: true },
});
if (!piece) {
throw new Error(`Pièce introuvable (${pieceId}).`);
}
const constructeurId = await this.resolveConstructeurId(
definition.constructeurId ?? definition.constructeur,
);
if (constructeurId) {
dto.constructeurId = constructeurId;
if (
requirement.typePieceId &&
piece.typePieceId &&
piece.typePieceId !== requirement.typePieceId
) {
throw new Error(
`La pièce sélectionnée (${piece.name || piece.id}) n'appartient pas à la famille attendue pour ce requirement.`,
);
}
const prix = this.normalizePrice(definition.prix);
if (prix !== undefined) {
dto.prix = prix;
}
return dto;
await this.prisma.piece.update({
where: { id: piece.id },
data: {
machineId,
composantId: null,
typeMachinePieceRequirementId: requirement.id,
},
});
}
private async createComponentsForMachine(
@@ -500,12 +500,20 @@ export class MachinesService {
for (const requirement of requirements) {
const selections = selectionMap.get(requirement.id) ?? [];
for (const selection of selections) {
const dto = await this.buildComponentCreationDto(
machineId,
requirement,
selection,
if (selection.composantId) {
await this.attachExistingComponentToMachine(
machineId,
requirement,
selection,
);
continue;
}
throw new Error(
`Aucun composant existant fourni pour le requirement "${
requirement.label || requirement.typeComposant?.name || requirement.id
}".`,
);
await this.composantsService.create(dto);
}
}
}
@@ -524,12 +532,20 @@ export class MachinesService {
for (const requirement of requirements) {
const selections = selectionMap.get(requirement.id) ?? [];
for (const selection of selections) {
const dto = await this.buildPieceCreationDto(
machineId,
requirement,
selection,
if (selection.pieceId) {
await this.attachExistingPieceToMachine(
machineId,
requirement,
selection,
);
continue;
}
throw new Error(
`Aucune pièce existante fournie pour le requirement "${
requirement.label || requirement.typePiece?.name || requirement.id
}".`,
);
await this.piecesService.create(dto);
}
}
}
@@ -1048,12 +1064,13 @@ export class MachinesService {
});
for (const customField of pieceCustomFields) {
const existingValue = await this.prisma.customFieldValue.findFirst({
where: {
customFieldId: customField.id,
pieceId: piece.id,
},
});
const existingValue =
await this.prisma.customFieldValue.findFirst({
where: {
customFieldId: customField.id,
pieceId: piece.id,
},
});
if (!existingValue) {
const providedValue = this.extractCustomFieldValue(
@@ -1076,10 +1093,12 @@ export class MachinesService {
}
for (const piece of machine.pieces) {
const typePiece = machinePieces.find(
(p: any) => p.name === piece.name,
);
if (typePiece && typePiece.customFields && typePiece.customFields.length > 0) {
const typePiece = machinePieces.find((p: any) => p.name === piece.name);
if (
typePiece &&
typePiece.customFields &&
typePiece.customFields.length > 0
) {
const typePieceFields = Array.isArray(typePiece.customFields)
? typePiece.customFields
: [];

View File

@@ -25,4 +25,7 @@ export class CreateModelTypeDto {
@IsOptional()
@IsString()
description?: string;
@IsOptional()
structure?: any;
}

View File

@@ -1,4 +1,5 @@
import {
BadRequestException,
ConflictException,
Injectable,
NotFoundException,
@@ -7,6 +8,10 @@ import { ModelType as PrismaModelType, Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import { CreateModelTypeDto, ModelCategory } from './dto/create-model-type.dto';
import { UpdateModelTypeDto } from './dto/update-model-type.dto';
import {
ComponentModelStructureSchema,
PieceModelStructureSchema,
} from '../shared/schemas/inventory';
type SortField = 'name' | 'code' | 'createdAt';
type SortDirection = 'asc' | 'desc';
@@ -31,7 +36,7 @@ export class ModelTypeService {
constructor(private readonly prisma: PrismaService) {}
async list(params: ListParams): Promise<{
items: PrismaModelType[];
items: ReturnType<ModelTypeService['mapModelType']>[];
total: number;
offset: number;
limit: number;
@@ -78,41 +83,110 @@ export class ModelTypeService {
]);
return {
items,
items: items.map((item) => this.mapModelType(item)),
total,
offset: safeOffset,
limit: cappedLimit,
};
}
async create(dto: CreateModelTypeDto): Promise<PrismaModelType> {
async create(
dto: CreateModelTypeDto,
): Promise<ReturnType<ModelTypeService['mapModelType']>> {
try {
return await this.prisma.modelType.create({
data: {
name: dto.name,
code: dto.code,
category: dto.category,
notes: dto.notes,
description: dto.description ?? null,
},
});
const { structure, ...rest } = dto;
const data: Prisma.ModelTypeCreateInput = {
name: rest.name,
code: rest.code,
category: rest.category,
notes: rest.notes,
description: rest.description ?? null,
};
const normalizedStructure = this.normalizeStructure(
rest.category,
structure,
);
if (normalizedStructure !== undefined) {
const skeletonValue =
normalizedStructure === null ? Prisma.JsonNull : normalizedStructure;
if (rest.category === ModelCategory.COMPONENT) {
data.componentSkeleton = skeletonValue;
data.pieceSkeleton = Prisma.JsonNull;
} else {
data.pieceSkeleton = skeletonValue;
data.componentSkeleton = Prisma.JsonNull;
}
}
const created = await this.prisma.modelType.create({ data });
return this.mapModelType(created);
} catch (error) {
this.handlePrismaError(error);
}
}
async update(id: string, dto: UpdateModelTypeDto): Promise<PrismaModelType> {
async update(
id: string,
dto: UpdateModelTypeDto,
): Promise<ReturnType<ModelTypeService['mapModelType']>> {
try {
return await this.prisma.modelType.update({
const existing = await this.prisma.modelType.findUnique({
where: { id },
data: {
...dto,
description:
dto.description === undefined
? undefined
: (dto.description ?? null),
},
});
if (!existing) {
throw new NotFoundException('Type de modèle introuvable.');
}
const targetCategory =
dto.category ?? (existing.category as ModelCategory);
const data: Prisma.ModelTypeUpdateInput = {};
if (dto.name !== undefined) {
data.name = dto.name;
}
if (dto.code !== undefined) {
data.code = dto.code;
}
if (dto.category !== undefined) {
data.category = dto.category;
}
if (dto.notes !== undefined) {
data.notes = dto.notes;
}
data.description =
dto.description === undefined ? undefined : (dto.description ?? null);
const normalizedStructure = this.normalizeStructure(
targetCategory,
dto.structure,
);
if (normalizedStructure !== undefined) {
const skeletonValue =
normalizedStructure === null ? Prisma.JsonNull : normalizedStructure;
if (targetCategory === ModelCategory.COMPONENT) {
data.componentSkeleton = skeletonValue;
data.pieceSkeleton = Prisma.JsonNull;
} else {
data.pieceSkeleton = skeletonValue;
data.componentSkeleton = Prisma.JsonNull;
}
}
const updated = await this.prisma.modelType.update({
where: { id },
data,
});
return this.mapModelType(updated);
} catch (error) {
this.handlePrismaError(error);
}
@@ -126,12 +200,14 @@ export class ModelTypeService {
}
}
async findOne(id: string): Promise<PrismaModelType> {
async findOne(
id: string,
): Promise<ReturnType<ModelTypeService['mapModelType']>> {
const modelType = await this.prisma.modelType.findUnique({ where: { id } });
if (!modelType) {
throw new NotFoundException('Type de modèle introuvable.');
}
return modelType;
return this.mapModelType(modelType);
}
private handlePrismaError(error: unknown): never {
@@ -158,4 +234,44 @@ export class ModelTypeService {
}
return false;
}
private normalizeStructure(
category: ModelCategory,
structure: unknown,
): Prisma.InputJsonValue | null | undefined {
if (structure === undefined) {
return undefined;
}
if (structure === null) {
return null;
}
try {
if (category === ModelCategory.COMPONENT) {
return ComponentModelStructureSchema.parse(
structure,
) as Prisma.InputJsonValue;
}
return PieceModelStructureSchema.parse(
structure,
) as Prisma.InputJsonValue;
} catch (error) {
const message =
error instanceof Error ? error.message : 'Structure invalide.';
throw new BadRequestException(message);
}
}
private mapModelType(modelType: PrismaModelType) {
const structure =
modelType.category === ModelCategory.COMPONENT
? (modelType.componentSkeleton ?? null)
: (modelType.pieceSkeleton ?? null);
return {
...modelType,
structure,
} as PrismaModelType & { structure: Prisma.InputJsonValue | null };
}
}

View File

@@ -98,9 +98,7 @@ describe('PiecesService', () => {
prisma.customField.findMany
.mockResolvedValueOnce([])
.mockResolvedValueOnce([
{ id: 'field-1', name: 'Numéro de série' },
]);
.mockResolvedValueOnce([{ id: 'field-1', name: 'Numéro de série' }]);
prisma.customField.create.mockResolvedValue({ id: 'field-1' });
prisma.customFieldValue.findMany.mockResolvedValue([]);
prisma.customFieldValue.create.mockResolvedValue({

View File

@@ -36,9 +36,15 @@ export class PiecesService {
constructor(private prisma: PrismaService) {}
async create(createPieceDto: CreatePieceDto) {
const requirementId = createPieceDto.typeMachinePieceRequirementId;
const requirementId = createPieceDto.typeMachinePieceRequirementId ?? null;
let machineId = createPieceDto.machineId;
if (requirementId && !createPieceDto.machineId) {
throw new BadRequestException(
'Un requirement ne peut pas être utilisé sans machine ciblée.',
);
}
let machineId = createPieceDto.machineId ?? null;
if (createPieceDto.composantId) {
const composantMachineId = await this.resolveMachineIdFromComposant(
@@ -51,59 +57,69 @@ export class PiecesService {
);
}
machineId = composantMachineId;
machineId = composantMachineId ?? machineId;
}
if (!machineId) {
throw new BadRequestException(
'Un machineId ou un composantId valide est requis pour créer une pièce.',
);
}
let requirement: PieceRequirementWithType | null = null;
const machine = await this.prisma.machine.findUnique({
where: { id: machineId },
include: {
typeMachine: {
include: {
pieceRequirements: {
include: {
typePiece: true,
if (machineId) {
const machine = await this.prisma.machine.findUnique({
where: { id: machineId },
include: {
typeMachine: {
include: {
pieceRequirements: {
include: {
typePiece: true,
},
},
},
},
},
},
});
});
if (!machine || !machine.typeMachine) {
throw new BadRequestException(
'La machine ciblée doit être associée à un type de machine pour valider les requirements.',
);
if (!machine || !machine.typeMachine) {
throw new BadRequestException(
'La machine ciblée doit être associée à un type de machine pour valider les requirements.',
);
}
if (requirementId) {
requirement =
(
machine.typeMachine.pieceRequirements as PieceRequirementWithType[]
).find((pieceRequirement) => pieceRequirement.id === requirementId) ??
null;
if (!requirement) {
throw new BadRequestException(
'Le requirement de pièce fourni ne correspond pas au squelette de la machine.',
);
}
if (
createPieceDto.typePieceId &&
createPieceDto.typePieceId !== requirement.typePieceId
) {
throw new BadRequestException(
'Le type de pièce fourni ne correspond pas au requirement pour cette machine.',
);
}
}
}
const requirement = machine.typeMachine.pieceRequirements.find(
(pieceRequirement) => pieceRequirement.id === requirementId,
);
const typePieceId =
createPieceDto.typePieceId ?? requirement?.typePieceId ?? null;
if (!requirement) {
throw new BadRequestException(
'Le requirement de pièce fourni ne correspond pas au squelette de la machine.',
);
}
if (
createPieceDto.typePieceId &&
createPieceDto.typePieceId !== requirement.typePieceId
) {
throw new BadRequestException(
'Le type de pièce fourni ne correspond pas au requirement pour cette machine.',
);
}
const data = {
...createPieceDto,
const data: Prisma.PieceUncheckedCreateInput = {
name: createPieceDto.name,
reference: createPieceDto.reference ?? null,
constructeurId: createPieceDto.constructeurId ?? null,
prix: createPieceDto.prix !== undefined ? createPieceDto.prix : null,
machineId,
typePieceId: createPieceDto.typePieceId ?? requirement.typePieceId,
composantId: createPieceDto.composantId ?? null,
typePieceId,
typeMachinePieceRequirementId: requirement?.id ?? requirementId ?? null,
};
const created = await this.prisma.piece.create({
@@ -113,10 +129,7 @@ export class PiecesService {
await this.applyPieceSkeleton({
pieceId: created.id,
typePiece:
(requirement.typePiece as PieceTypeWithSkeleton | null) ??
(created.typePiece as PieceTypeWithSkeleton | null) ??
null,
typePiece: created.typePiece as PieceTypeWithSkeleton | null,
});
return this.prisma.piece.findUnique({
@@ -217,8 +230,8 @@ export class PiecesService {
}
const skeleton = this.parsePieceSkeleton(
(typePiece as { pieceSkeleton?: Prisma.JsonValue | null } | null)?.
pieceSkeleton,
(typePiece as { pieceSkeleton?: Prisma.JsonValue | null } | null)
?.pieceSkeleton,
);
if (!skeleton) {
@@ -227,10 +240,7 @@ export class PiecesService {
const customFields = skeleton.customFields ?? [];
await this.ensurePieceCustomFieldDefinitions(
typePiece.id,
customFields,
);
await this.ensurePieceCustomFieldDefinitions(typePiece.id, customFields);
await this.createPieceCustomFieldValues(
pieceId,
@@ -255,7 +265,11 @@ export class PiecesService {
typePieceId: string,
customFields: PieceModelStructure['customFields'],
) {
if (!typePieceId || !Array.isArray(customFields) || customFields.length === 0) {
if (
!typePieceId ||
!Array.isArray(customFields) ||
customFields.length === 0
) {
return;
}
@@ -265,7 +279,10 @@ export class PiecesService {
});
const existingByName = new Map(
existing.map((field) => [this.normalizeIdentifier(field.name) ?? field.name, field.id]),
existing.map((field) => [
this.normalizeIdentifier(field.name) ?? field.name,
field.id,
]),
);
for (const field of customFields) {
@@ -306,7 +323,11 @@ export class PiecesService {
typePieceId: string,
customFields: PieceModelStructure['customFields'],
) {
if (!typePieceId || !Array.isArray(customFields) || customFields.length === 0) {
if (
!typePieceId ||
!Array.isArray(customFields) ||
customFields.length === 0
) {
return;
}
@@ -320,7 +341,10 @@ export class PiecesService {
}
const definitionMap = new Map(
definitions.map((field) => [this.normalizeIdentifier(field.name) ?? field.name, field.id]),
definitions.map((field) => [
this.normalizeIdentifier(field.name) ?? field.name,
field.id,
]),
);
const existingValues = await this.prisma.customFieldValue.findMany({
@@ -328,7 +352,9 @@ export class PiecesService {
select: { customFieldId: true },
});
const existingIds = new Set(existingValues.map((value) => value.customFieldId));
const existingIds = new Set(
existingValues.map((value) => value.customFieldId),
);
for (const field of customFields) {
if (!field) {
@@ -363,9 +389,7 @@ export class PiecesService {
const rawOptions = field?.options;
if (Array.isArray(rawOptions)) {
const normalized = rawOptions
.map((option) =>
typeof option === 'string' ? option.trim() : '',
)
.map((option) => (typeof option === 'string' ? option.trim() : ''))
.filter((option) => option.length > 0);
return normalized.length > 0 ? normalized : undefined;

View File

@@ -1,15 +1,15 @@
import { IsString, IsOptional, IsNumber, ValidateIf } from 'class-validator';
import { IsString, IsOptional, IsNumber } from 'class-validator';
import { Transform } from 'class-transformer';
export class CreateComposantDto {
@IsString()
name: string;
@ValidateIf((dto) => !dto.parentComposantId)
@IsOptional()
@IsString()
machineId?: string;
@ValidateIf((dto) => !dto.machineId)
@IsOptional()
@IsString()
parentComposantId?: string;
@@ -30,8 +30,9 @@ export class CreateComposantDto {
@IsString()
typeComposantId?: string;
@IsOptional()
@IsString()
typeMachineComponentRequirementId: string;
typeMachineComponentRequirementId?: string;
}
export class UpdateComposantDto {

View File

@@ -10,6 +10,10 @@ export class MachineComponentSelectionDto {
@IsString()
typeComposantId?: string;
@IsOptional()
@IsString()
composantId?: string;
@IsOptional()
definition?: any;
}
@@ -22,6 +26,10 @@ export class MachinePieceSelectionDto {
@IsString()
typePieceId?: string;
@IsOptional()
@IsString()
pieceId?: string;
@IsOptional()
definition?: any;
}

View File

@@ -1,15 +1,15 @@
import { IsString, IsOptional, IsNumber, ValidateIf } from 'class-validator';
import { IsString, IsOptional, IsNumber } from 'class-validator';
import { Transform } from 'class-transformer';
export class CreatePieceDto {
@IsString()
name: string;
@ValidateIf((dto) => !dto.composantId)
@IsOptional()
@IsString()
machineId?: string;
@ValidateIf((dto) => !dto.machineId)
@IsOptional()
@IsString()
composantId?: string;
@@ -30,8 +30,9 @@ export class CreatePieceDto {
@IsString()
typePieceId?: string;
@IsOptional()
@IsString()
typeMachinePieceRequirementId: string;
typeMachinePieceRequirementId?: string;
}
export class UpdatePieceDto {

View File

@@ -258,4 +258,3 @@ export class UpdateTypePieceDto {
@IsObject()
structure?: PieceModelStructure;
}

View File

@@ -1,4 +1,12 @@
import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
} from '@nestjs/common';
import { TypesService } from './types.service';
import {
CreateTypeMachineDto,