From 48a74b74d7f1cf5319d6ea7b42530f2e8a8db777 Mon Sep 17 00:00:00 2001 From: Matthieu Date: Wed, 8 Oct 2025 16:23:49 +0200 Subject: [PATCH] refactor: prepare multi-machine inventory associations --- scripts/cleanup-custom-fields.ts | 42 ++ scripts/seed-basic-categories.ts | 593 +++++++++++++++++++ src/common/mappers/model-type.mapper.ts | 4 +- src/component-models/structure.normalizer.ts | 36 +- src/composants/composants.service.spec.ts | 8 +- src/composants/composants.service.ts | 219 ++++--- src/custom-fields/custom-fields.service.ts | 4 +- src/machines/machines.controller.spec.ts | 4 +- src/machines/machines.service.spec.ts | 4 +- src/machines/machines.service.ts | 197 +++--- src/model-type/dto/create-model-type.dto.ts | 3 + src/model-type/model-type.service.ts | 162 ++++- src/pieces/pieces.service.spec.ts | 4 +- src/pieces/pieces.service.ts | 146 +++-- src/shared/dto/composant.dto.ts | 9 +- src/shared/dto/machine.dto.ts | 8 + src/shared/dto/piece.dto.ts | 9 +- src/shared/dto/type.dto.ts | 1 - src/types/types.controller.ts | 10 +- 19 files changed, 1166 insertions(+), 297 deletions(-) create mode 100644 scripts/cleanup-custom-fields.ts create mode 100644 scripts/seed-basic-categories.ts diff --git a/scripts/cleanup-custom-fields.ts b/scripts/cleanup-custom-fields.ts new file mode 100644 index 0000000..7255dfd --- /dev/null +++ b/scripts/cleanup-custom-fields.ts @@ -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() diff --git a/scripts/seed-basic-categories.ts b/scripts/seed-basic-categories.ts new file mode 100644 index 0000000..c886bb5 --- /dev/null +++ b/scripts/seed-basic-categories.ts @@ -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) { + type PieceSkeleton = { + customFields?: Array<{ name: string; value?: unknown; type?: string; required?: boolean; options?: unknown }>; + [key: string]: unknown; + }; + + const definitions: Record = { + '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, + pieceMap: Map, +) { + 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 = { + '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, + 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, + 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, + pieceMap: Map, +) { + 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(); + }); diff --git a/src/common/mappers/model-type.mapper.ts b/src/common/mappers/model-type.mapper.ts index 3fd3417..ca64975 100644 --- a/src/common/mappers/model-type.mapper.ts +++ b/src/common/mappers/model-type.mapper.ts @@ -50,7 +50,9 @@ export class ModelTypeMapper { })), } : undefined, - ...(skeleton ? { componentSkeleton: skeleton as Prisma.InputJsonValue } : {}), + ...(skeleton + ? { componentSkeleton: skeleton as Prisma.InputJsonValue } + : {}), }; } diff --git a/src/component-models/structure.normalizer.ts b/src/component-models/structure.normalizer.ts index 75bff4f..1b3fa40 100644 --- a/src/component-models/structure.normalizer.ts +++ b/src/component-models/structure.normalizer.ts @@ -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 | 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 | 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 | null | undefined; + const candidate = subcomponent as + | Record + | 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]; }); diff --git a/src/composants/composants.service.spec.ts b/src/composants/composants.service.spec.ts index 000faf9..587b86b 100644 --- a/src/composants/composants.service.spec.ts +++ b/src/composants/composants.service.spec.ts @@ -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 = { diff --git a/src/composants/composants.service.ts b/src/composants/composants.service.ts index 5a1c69f..6ac4059 100644 --- a/src/composants/composants.service.ts +++ b/src/composants/composants.service.ts @@ -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(); - componentRequirementUsage.set(requirement.id, 1); - const pieceRequirementUsage = new Map(); - - 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(); + componentRequirementUsage.set(requirement.id, 1); + const pieceRequirementUsage = new Map(); + + 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; }) { 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; } diff --git a/src/custom-fields/custom-fields.service.ts b/src/custom-fields/custom-fields.service.ts index 5b6ba5d..9fc15ab 100644 --- a/src/custom-fields/custom-fields.service.ts +++ b/src/custom-fields/custom-fields.service.ts @@ -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, diff --git a/src/machines/machines.controller.spec.ts b/src/machines/machines.controller.spec.ts index 79e305c..193b8e7 100644 --- a/src/machines/machines.controller.spec.ts +++ b/src/machines/machines.controller.spec.ts @@ -9,7 +9,9 @@ describe('MachinesController', () => { let controller: MachinesController; beforeEach(async () => { - const mockComposantsService = { create: jest.fn() } as Partial; + const mockComposantsService = { + create: jest.fn(), + } as Partial; const mockPiecesService = { create: jest.fn() } as Partial; const module: TestingModule = await Test.createTestingModule({ diff --git a/src/machines/machines.service.spec.ts b/src/machines/machines.service.spec.ts index cec433a..a95a8d5 100644 --- a/src/machines/machines.service.spec.ts +++ b/src/machines/machines.service.spec.ts @@ -8,7 +8,9 @@ describe('MachinesService', () => { let service: MachinesService; beforeEach(async () => { - const mockComposantsService = { create: jest.fn() } as Partial; + const mockComposantsService = { + create: jest.fn(), + } as Partial; const mockPiecesService = { create: jest.fn() } as Partial; const module: TestingModule = await Test.createTestingModule({ diff --git a/src/machines/machines.service.ts b/src/machines/machines.service.ts index 3fb8cbf..dfc0519 100644 --- a/src/machines/machines.service.ts +++ b/src/machines/machines.service.ts @@ -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 { - 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 { - 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 : []; diff --git a/src/model-type/dto/create-model-type.dto.ts b/src/model-type/dto/create-model-type.dto.ts index 87c92d0..7454dc8 100644 --- a/src/model-type/dto/create-model-type.dto.ts +++ b/src/model-type/dto/create-model-type.dto.ts @@ -25,4 +25,7 @@ export class CreateModelTypeDto { @IsOptional() @IsString() description?: string; + + @IsOptional() + structure?: any; } diff --git a/src/model-type/model-type.service.ts b/src/model-type/model-type.service.ts index 039abe9..61bd0a1 100644 --- a/src/model-type/model-type.service.ts +++ b/src/model-type/model-type.service.ts @@ -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[]; 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 { + async create( + dto: CreateModelTypeDto, + ): Promise> { 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 { + async update( + id: string, + dto: UpdateModelTypeDto, + ): Promise> { 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 { + async findOne( + id: string, + ): Promise> { 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 }; + } } diff --git a/src/pieces/pieces.service.spec.ts b/src/pieces/pieces.service.spec.ts index 01ad300..e7d927e 100644 --- a/src/pieces/pieces.service.spec.ts +++ b/src/pieces/pieces.service.spec.ts @@ -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({ diff --git a/src/pieces/pieces.service.ts b/src/pieces/pieces.service.ts index fed4ced..cfea433 100644 --- a/src/pieces/pieces.service.ts +++ b/src/pieces/pieces.service.ts @@ -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; diff --git a/src/shared/dto/composant.dto.ts b/src/shared/dto/composant.dto.ts index f902486..c842868 100644 --- a/src/shared/dto/composant.dto.ts +++ b/src/shared/dto/composant.dto.ts @@ -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 { diff --git a/src/shared/dto/machine.dto.ts b/src/shared/dto/machine.dto.ts index ad98e24..99c58e2 100644 --- a/src/shared/dto/machine.dto.ts +++ b/src/shared/dto/machine.dto.ts @@ -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; } diff --git a/src/shared/dto/piece.dto.ts b/src/shared/dto/piece.dto.ts index 8481259..ab56326 100644 --- a/src/shared/dto/piece.dto.ts +++ b/src/shared/dto/piece.dto.ts @@ -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 { diff --git a/src/shared/dto/type.dto.ts b/src/shared/dto/type.dto.ts index 222286a..eb3ccbb 100644 --- a/src/shared/dto/type.dto.ts +++ b/src/shared/dto/type.dto.ts @@ -258,4 +258,3 @@ export class UpdateTypePieceDto { @IsObject() structure?: PieceModelStructure; } - diff --git a/src/types/types.controller.ts b/src/types/types.controller.ts index 5f78c97..c2e1d90 100644 --- a/src/types/types.controller.ts +++ b/src/types/types.controller.ts @@ -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,