feat: Add Model gestion for piece and component

This commit is contained in:
Matthieu
2025-09-23 15:05:33 +02:00
parent bc225bf3e8
commit e64fba3ae7
54 changed files with 1640 additions and 569 deletions

21
package-lock.json generated
View File

@@ -12,6 +12,7 @@
"@nestjs/common": "^11.0.1",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.1",
"@nestjs/mapped-types": "^2.0.5",
"@nestjs/platform-express": "^11.0.1",
"@nestjs/typeorm": "^11.0.0",
"@prisma/client": "^6.12.0",
@@ -2535,6 +2536,26 @@
}
}
},
"node_modules/@nestjs/mapped-types": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.1.0.tgz",
"integrity": "sha512-W+n+rM69XsFdwORF11UqJahn4J3xi4g/ZEOlJNL6KoW5ygWSmBB2p0S2BZ4FQeS/NDH72e6xIcu35SfJnE8bXw==",
"license": "MIT",
"peerDependencies": {
"@nestjs/common": "^10.0.0 || ^11.0.0",
"class-transformer": "^0.4.0 || ^0.5.0",
"class-validator": "^0.13.0 || ^0.14.0",
"reflect-metadata": "^0.1.12 || ^0.2.0"
},
"peerDependenciesMeta": {
"class-transformer": {
"optional": true
},
"class-validator": {
"optional": true
}
}
},
"node_modules/@nestjs/platform-express": {
"version": "11.1.5",
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.5.tgz",

View File

@@ -23,6 +23,7 @@
"@nestjs/common": "^11.0.1",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.1",
"@nestjs/mapped-types": "^2.0.5",
"@nestjs/platform-express": "^11.0.1",
"@nestjs/typeorm": "^11.0.0",
"@prisma/client": "^6.12.0",

View File

@@ -0,0 +1,21 @@
-- CreateEnum
CREATE TYPE "ModelCategory" AS ENUM ('COMPONENT', 'PIECE');
-- CreateTable
CREATE TABLE "ModelType" (
"id" TEXT NOT NULL,
"name" VARCHAR(120) NOT NULL,
"code" VARCHAR(60) NOT NULL,
"category" "ModelCategory" NOT NULL,
"notes" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "ModelType_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "ModelType_code_key" ON "ModelType"("code");
-- CreateIndex
CREATE INDEX "ModelType_category_name_idx" ON "ModelType"("category", "name");

View File

@@ -0,0 +1,35 @@
/*
Warnings:
- You are about to drop the `type_composants` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropForeignKey
ALTER TABLE "composant_models" DROP CONSTRAINT "composant_models_typeComposantId_fkey";
-- DropForeignKey
ALTER TABLE "composants" DROP CONSTRAINT "composants_typeComposantId_fkey";
-- DropForeignKey
ALTER TABLE "custom_fields" DROP CONSTRAINT "custom_fields_typeComposantId_fkey";
-- DropForeignKey
ALTER TABLE "type_machine_component_requirements" DROP CONSTRAINT "type_machine_component_requirements_typeComposantId_fkey";
-- AlterTable
ALTER TABLE "ModelType" ADD COLUMN "description" TEXT;
-- DropTable
DROP TABLE "type_composants";
-- AddForeignKey
ALTER TABLE "composants" ADD CONSTRAINT "composants_typeComposantId_fkey" FOREIGN KEY ("typeComposantId") REFERENCES "ModelType"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "custom_fields" ADD CONSTRAINT "custom_fields_typeComposantId_fkey" FOREIGN KEY ("typeComposantId") REFERENCES "ModelType"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "composant_models" ADD CONSTRAINT "composant_models_typeComposantId_fkey" FOREIGN KEY ("typeComposantId") REFERENCES "ModelType"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "type_machine_component_requirements" ADD CONSTRAINT "type_machine_component_requirements_typeComposantId_fkey" FOREIGN KEY ("typeComposantId") REFERENCES "ModelType"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -0,0 +1,43 @@
/*
Migration: migrate legacy TypePiece references to ModelType entries
*/
-- Insert existing type_pieces into ModelType if not already present
INSERT INTO "ModelType" ("id", "name", "code", "category", "notes", "description", "createdAt", "updatedAt")
SELECT
tp."id",
tp."name",
tp."id" AS "code",
'PIECE'::"ModelCategory",
tp."description",
tp."description",
tp."createdAt",
tp."updatedAt"
FROM "type_pieces" tp
ON CONFLICT ("id") DO NOTHING;
-- Drop existing foreign keys referencing type_pieces
ALTER TABLE "pieces" DROP CONSTRAINT IF EXISTS "pieces_typePieceId_fkey";
ALTER TABLE "custom_fields" DROP CONSTRAINT IF EXISTS "custom_fields_typePieceId_fkey";
ALTER TABLE "piece_models" DROP CONSTRAINT IF EXISTS "piece_models_typePieceId_fkey";
ALTER TABLE "type_machine_piece_requirements" DROP CONSTRAINT IF EXISTS "type_machine_piece_requirements_typePieceId_fkey";
-- Drop legacy table
DROP TABLE IF EXISTS "type_pieces";
-- Recreate foreign keys pointing to ModelType
ALTER TABLE "pieces"
ADD CONSTRAINT "pieces_typePieceId_fkey"
FOREIGN KEY ("typePieceId") REFERENCES "ModelType"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "custom_fields"
ADD CONSTRAINT "custom_fields_typePieceId_fkey"
FOREIGN KEY ("typePieceId") REFERENCES "ModelType"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "piece_models"
ADD CONSTRAINT "piece_models_typePieceId_fkey"
FOREIGN KEY ("typePieceId") REFERENCES "ModelType"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "type_machine_piece_requirements"
ADD CONSTRAINT "type_machine_piece_requirements_typePieceId_fkey"
FOREIGN KEY ("typePieceId") REFERENCES "ModelType"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -51,38 +51,6 @@ model TypeMachine {
@@map("type_machines")
}
model TypeComposant {
id String @id @default(cuid())
name String @unique
description String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
composants Composant[]
customFields CustomField[] @relation("TypeComposantCustomFields")
models ComposantModel[]
componentRequirements TypeMachineComponentRequirement[]
@@map("type_composants")
}
model TypePiece {
id String @id @default(cuid())
name String @unique
description String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
pieces Piece[]
customFields CustomField[] @relation("TypePieceCustomFields")
models PieceModel[]
pieceRequirements TypeMachinePieceRequirement[]
@@map("type_pieces")
}
model Machine {
id String @id @default(cuid())
name String
@@ -128,7 +96,7 @@ model Composant {
sousComposants Composant[] @relation("ComposantHierarchy")
typeComposantId String?
typeComposant TypeComposant? @relation(fields: [typeComposantId], references: [id])
typeComposant ModelType? @relation("ModelTypeComponentAssignments", fields: [typeComposantId], references: [id])
composantModelId String?
composantModel ComposantModel? @relation(fields: [composantModelId], references: [id], onDelete: SetNull)
@@ -163,7 +131,7 @@ model Piece {
composant Composant? @relation(fields: [composantId], references: [id], onDelete: Cascade)
typePieceId String?
typePiece TypePiece? @relation(fields: [typePieceId], references: [id])
typePiece ModelType? @relation("ModelTypePieceAssignments", fields: [typePieceId], references: [id])
pieceModelId String?
pieceModel PieceModel? @relation(fields: [pieceModelId], references: [id], onDelete: SetNull)
@@ -180,6 +148,33 @@ model Piece {
@@map("pieces")
}
enum ModelCategory {
COMPONENT
PIECE
}
model ModelType {
id String @id @default(cuid())
name String @db.VarChar(120)
code String @unique @db.VarChar(60)
category ModelCategory
notes String? @db.Text
description String? @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([category, name])
composants Composant[] @relation("ModelTypeComponentAssignments")
models ComposantModel[] @relation("ModelTypeComponentModels")
componentRequirements TypeMachineComponentRequirement[] @relation("ModelTypeComponentRequirements")
customFields CustomField[] @relation("ModelTypeCustomFields")
pieceModels PieceModel[] @relation("ModelTypePieceModels")
pieceRequirements TypeMachinePieceRequirement[] @relation("ModelTypePieceRequirements")
pieces Piece[] @relation("ModelTypePieceAssignments")
pieceCustomFields CustomField[] @relation("ModelTypePieceCustomFields")
}
model Constructeur {
id String @id @default(cuid())
name String @unique
@@ -247,10 +242,10 @@ model CustomField {
typeMachine TypeMachine? @relation("TypeMachineCustomFields", fields: [typeMachineId], references: [id], onDelete: Cascade)
typeComposantId String?
typeComposant TypeComposant? @relation("TypeComposantCustomFields", fields: [typeComposantId], references: [id], onDelete: Cascade)
typeComposant ModelType? @relation("ModelTypeCustomFields", fields: [typeComposantId], references: [id], onDelete: Cascade)
typePieceId String?
typePiece TypePiece? @relation("TypePieceCustomFields", fields: [typePieceId], references: [id], onDelete: Cascade)
typePiece ModelType? @relation("ModelTypePieceCustomFields", fields: [typePieceId], references: [id], onDelete: Cascade)
// Relations avec les valeurs
customFieldValues CustomFieldValue[]
@@ -289,7 +284,7 @@ model ComposantModel {
updatedAt DateTime @updatedAt
typeComposantId String
typeComposant TypeComposant @relation(fields: [typeComposantId], references: [id], onDelete: Cascade)
typeComposant ModelType @relation("ModelTypeComponentModels", fields: [typeComposantId], references: [id], onDelete: Cascade)
composants Composant[]
@@ -305,7 +300,7 @@ model PieceModel {
updatedAt DateTime @updatedAt
typePieceId String
typePiece TypePiece @relation(fields: [typePieceId], references: [id], onDelete: Cascade)
typePiece ModelType @relation("ModelTypePieceModels", fields: [typePieceId], references: [id], onDelete: Cascade)
pieces Piece[]
@@ -326,7 +321,7 @@ model TypeMachineComponentRequirement {
typeMachine TypeMachine @relation(fields: [typeMachineId], references: [id], onDelete: Cascade)
typeComposantId String
typeComposant TypeComposant @relation(fields: [typeComposantId], references: [id])
typeComposant ModelType @relation("ModelTypeComponentRequirements", fields: [typeComposantId], references: [id])
composants Composant[]
@@ -347,7 +342,7 @@ model TypeMachinePieceRequirement {
typeMachine TypeMachine @relation(fields: [typeMachineId], references: [id], onDelete: Cascade)
typePieceId String
typePiece TypePiece @relation(fields: [typePieceId], references: [id])
typePiece ModelType @relation("ModelTypePieceRequirements", fields: [typePieceId], references: [id])
pieces Piece[]

View File

@@ -13,6 +13,7 @@ import { CustomFieldsModule } from './custom-fields/custom-fields.module';
import { ConstructeursModule } from './constructeurs/constructeurs.module';
import { ProfilesModule } from './profiles/profiles.module';
import { SessionModule } from './session/session.module';
import { ModelTypeModule } from './model-type/model-type.module';
@Module({
imports: [
@@ -30,6 +31,7 @@ import { SessionModule } from './session/session.module';
ConstructeursModule,
ProfilesModule,
SessionModule,
ModelTypeModule,
],
controllers: [AppController],
providers: [AppService],

View File

@@ -1,6 +1,17 @@
import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
} from '@nestjs/common';
import { ComposantsService } from './composants.service';
import { CreateComposantDto, UpdateComposantDto } from '../shared/dto/composant.dto';
import {
CreateComposantDto,
UpdateComposantDto,
} from '../shared/dto/composant.dto';
@Controller('composants')
export class ComposantsController {
@@ -32,7 +43,10 @@ export class ComposantsController {
}
@Patch(':id')
update(@Param('id') id: string, @Body() updateComposantDto: UpdateComposantDto) {
update(
@Param('id') id: string,
@Body() updateComposantDto: UpdateComposantDto,
) {
return this.composantsService.update(id, updateComposantDto);
}

View File

@@ -4,6 +4,6 @@ import { ComposantsService } from './composants.service';
@Module({
controllers: [ComposantsController],
providers: [ComposantsService]
providers: [ComposantsService],
})
export class ComposantsModule {}

View File

@@ -20,7 +20,10 @@ describe('ComposantsService', () => {
};
const module: TestingModule = await Test.createTestingModule({
providers: [ComposantsService, { provide: PrismaService, useValue: prisma }],
providers: [
ComposantsService,
{ provide: PrismaService, useValue: prisma },
],
}).compile();
service = module.get<ComposantsService>(ComposantsService);
@@ -53,9 +56,9 @@ describe('ComposantsService', () => {
await expect(service.create(dto)).resolves.toEqual(created);
expect(prisma.composant.create).toHaveBeenCalled();
expect(
prisma.composant.create.mock.calls[0][0].data.typeComposantId,
).toBe('type-comp-1');
expect(prisma.composant.create.mock.calls[0][0].data.typeComposantId).toBe(
'type-comp-1',
);
});
it('should refuse creation when requirement does not belong to machine skeleton', async () => {

View File

@@ -1,6 +1,9 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { CreateComposantDto, UpdateComposantDto } from '../shared/dto/composant.dto';
import {
CreateComposantDto,
UpdateComposantDto,
} from '../shared/dto/composant.dto';
@Injectable()
export class ComposantsService {
@@ -324,9 +327,9 @@ export class ComposantsService {
async findHierarchy(machineId: string) {
// Récupérer tous les composants de premier niveau (sans parent)
const rootComposants = await this.prisma.composant.findMany({
where: {
where: {
machineId,
parentComposantId: null
parentComposantId: null,
},
include: {
typeComposant: true,

View File

@@ -1,6 +1,18 @@
import { Controller, Get, Post, Patch, Delete, Param, Body, Query } from '@nestjs/common'
import { ConstructeursService } from './constructeurs.service'
import { CreateConstructeurDto, UpdateConstructeurDto } from '../shared/dto/constructeur.dto'
import {
Controller,
Get,
Post,
Patch,
Delete,
Param,
Body,
Query,
} from '@nestjs/common';
import { ConstructeursService } from './constructeurs.service';
import {
CreateConstructeurDto,
UpdateConstructeurDto,
} from '../shared/dto/constructeur.dto';
@Controller('constructeurs')
export class ConstructeursController {
@@ -8,26 +20,26 @@ export class ConstructeursController {
@Post()
create(@Body() payload: CreateConstructeurDto) {
return this.constructeursService.create(payload)
return this.constructeursService.create(payload);
}
@Get()
findAll(@Query('search') search?: string) {
return this.constructeursService.findAll(search)
return this.constructeursService.findAll(search);
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.constructeursService.findOne(id)
return this.constructeursService.findOne(id);
}
@Patch(':id')
update(@Param('id') id: string, @Body() payload: UpdateConstructeurDto) {
return this.constructeursService.update(id, payload)
return this.constructeursService.update(id, payload);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.constructeursService.remove(id)
return this.constructeursService.remove(id);
}
}

View File

@@ -1,6 +1,6 @@
import { Module } from '@nestjs/common'
import { ConstructeursService } from './constructeurs.service'
import { ConstructeursController } from './constructeurs.controller'
import { Module } from '@nestjs/common';
import { ConstructeursService } from './constructeurs.service';
import { ConstructeursController } from './constructeurs.controller';
@Module({
controllers: [ConstructeursController],

View File

@@ -1,23 +1,26 @@
import { Injectable } from '@nestjs/common'
import { PrismaService } from '../prisma/prisma.service'
import { CreateConstructeurDto, UpdateConstructeurDto } from '../shared/dto/constructeur.dto'
import { Prisma } from '@prisma/client'
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import {
CreateConstructeurDto,
UpdateConstructeurDto,
} from '../shared/dto/constructeur.dto';
import { Prisma } from '@prisma/client';
@Injectable()
export class ConstructeursService {
constructor(private prisma: PrismaService) {}
private buildSearchWhere(search?: string): Prisma.ConstructeurWhereInput {
if (!search) return {}
const term = search.trim()
if (!term) return {}
if (!search) return {};
const term = search.trim();
if (!term) return {};
return {
OR: [
{ name: { contains: term, mode: 'insensitive' } },
{ email: { contains: term, mode: 'insensitive' } },
{ phone: { contains: term, mode: 'insensitive' } },
],
}
};
}
async create(data: CreateConstructeurDto) {
@@ -28,20 +31,20 @@ export class ConstructeursService {
email: data.email?.trim(),
phone: data.phone?.trim(),
},
})
});
}
async findAll(search?: string) {
return this.prisma.constructeur.findMany({
where: this.buildSearchWhere(search),
orderBy: { name: 'asc' },
})
});
}
async findOne(id: string) {
return this.prisma.constructeur.findUnique({
where: { id },
})
});
}
async update(id: string, data: UpdateConstructeurDto) {
@@ -53,12 +56,12 @@ export class ConstructeursService {
email: data.email?.trim(),
phone: data.phone?.trim(),
},
})
});
}
async remove(id: string) {
return this.prisma.constructeur.delete({
where: { id },
})
});
}
}

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 { CustomFieldsService } from './custom-fields.service';
import {
CreateCustomFieldValueDto,
@@ -12,8 +20,12 @@ export class CustomFieldsController {
constructor(private readonly customFieldsService: CustomFieldsService) {}
@Post('values')
createCustomFieldValue(@Body() createCustomFieldValueDto: CreateCustomFieldValueDto) {
return this.customFieldsService.createCustomFieldValue(createCustomFieldValueDto);
createCustomFieldValue(
@Body() createCustomFieldValueDto: CreateCustomFieldValueDto,
) {
return this.customFieldsService.createCustomFieldValue(
createCustomFieldValueDto,
);
}
@Get('values/:entityType/:entityId')
@@ -34,7 +46,10 @@ export class CustomFieldsController {
@Param('id') id: string,
@Body() updateCustomFieldValueDto: UpdateCustomFieldValueDto,
) {
return this.customFieldsService.updateCustomFieldValue(id, updateCustomFieldValueDto);
return this.customFieldsService.updateCustomFieldValue(
id,
updateCustomFieldValueDto,
);
}
@Delete('values/:id')
@@ -51,4 +66,4 @@ export class CustomFieldsController {
body.value,
);
}
}
}

View File

@@ -9,4 +9,4 @@ import { PrismaModule } from '../prisma/prisma.module';
providers: [CustomFieldsService],
exports: [CustomFieldsService],
})
export class CustomFieldsModule {}
export class CustomFieldsModule {}

View File

@@ -1,4 +1,8 @@
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import {
BadRequestException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import {
CreateCustomFieldValueDto,
@@ -11,7 +15,9 @@ export class CustomFieldsService {
constructor(private prisma: PrismaService) {}
// Créer une valeur de champ personnalisé
async createCustomFieldValue(createCustomFieldValueDto: CreateCustomFieldValueDto) {
async createCustomFieldValue(
createCustomFieldValueDto: CreateCustomFieldValueDto,
) {
return this.prisma.customFieldValue.create({
data: createCustomFieldValueDto,
include: {
@@ -30,11 +36,16 @@ export class CustomFieldsService {
case CustomFieldEntityType.PIECE:
return 'pieceId' as const;
default:
throw new BadRequestException('Type d\'entité de champ personnalisé invalide.');
throw new BadRequestException(
"Type d'entité de champ personnalisé invalide.",
);
}
}
private async resolveEntityContext(entityType: CustomFieldEntityType, entityId: string) {
private async resolveEntityContext(
entityType: CustomFieldEntityType,
entityId: string,
) {
switch (entityType) {
case CustomFieldEntityType.MACHINE: {
const machine = await this.prisma.machine.findUnique({
@@ -103,7 +114,9 @@ export class CustomFieldsService {
};
}
default:
throw new BadRequestException('Type d\'entité de champ personnalisé invalide.');
throw new BadRequestException(
"Type d'entité de champ personnalisé invalide.",
);
}
}
@@ -135,7 +148,10 @@ export class CustomFieldsService {
}
// Mettre à jour une valeur de champ personnalisé
async updateCustomFieldValue(id: string, updateCustomFieldValueDto: UpdateCustomFieldValueDto) {
async updateCustomFieldValue(
id: string,
updateCustomFieldValueDto: UpdateCustomFieldValueDto,
) {
return this.prisma.customFieldValue.update({
where: { id },
data: updateCustomFieldValueDto,
@@ -159,10 +175,8 @@ export class CustomFieldsService {
entityId: string,
value: string,
) {
const { typeId, customFieldTypeField, valueKey } = await this.resolveEntityContext(
entityType,
entityId,
);
const { typeId, customFieldTypeField, valueKey } =
await this.resolveEntityContext(entityType, entityId);
const allowedCustomField = await this.prisma.customField.findFirst({
where: {
@@ -173,7 +187,7 @@ export class CustomFieldsService {
if (!allowedCustomField) {
throw new BadRequestException(
'Le champ personnalisé n\'est pas autorisé pour cette entité.',
"Le champ personnalisé n'est pas autorisé pour cette entité.",
);
}

View File

@@ -1,6 +1,17 @@
import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
} from '@nestjs/common';
import { DocumentsService } from './documents.service';
import { CreateDocumentDto, UpdateDocumentDto } from '../shared/dto/document.dto';
import {
CreateDocumentDto,
UpdateDocumentDto,
} from '../shared/dto/document.dto';
@Controller('documents')
export class DocumentsController {
@@ -42,7 +53,10 @@ export class DocumentsController {
}
@Patch(':id')
update(@Param('id') id: string, @Body() updateDocumentDto: UpdateDocumentDto) {
update(
@Param('id') id: string,
@Body() updateDocumentDto: UpdateDocumentDto,
) {
return this.documentsService.update(id, updateDocumentDto);
}

View File

@@ -4,6 +4,6 @@ import { DocumentsService } from './documents.service';
@Module({
controllers: [DocumentsController],
providers: [DocumentsService]
providers: [DocumentsService],
})
export class DocumentsModule {}

View File

@@ -1,6 +1,9 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { CreateDocumentDto, UpdateDocumentDto } from '../shared/dto/document.dto';
import {
CreateDocumentDto,
UpdateDocumentDto,
} from '../shared/dto/document.dto';
@Injectable()
export class DocumentsService {

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 { MachinesService } from './machines.service';
import {
CreateMachineDto,

View File

@@ -4,6 +4,6 @@ import { MachinesService } from './machines.service';
@Module({
controllers: [MachinesController],
providers: [MachinesService]
providers: [MachinesService],
})
export class MachinesModule {}

View File

@@ -1,4 +1,5 @@
import { Injectable } from '@nestjs/common';
import { ModelCategory } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import {
CreateMachineDto,
@@ -90,6 +91,36 @@ const MACHINE_DEFAULT_INCLUDE = {
export class MachinesService {
constructor(private prisma: PrismaService) {}
private slugifyName(name: string): string {
return name
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
.replace(/-+/g, '-');
}
private async generateUniqueComponentTypeCode(
prisma: any,
name: string,
): Promise<string> {
const base = this.slugifyName(name) || 'type';
let candidate = base;
let suffix = 1;
while (
await prisma.modelType.findUnique({
where: { code: candidate },
select: { id: true },
})
) {
candidate = `${base}-${suffix++}`;
}
return candidate;
}
private async getTypeMachineConfiguration(typeMachineId: string) {
const typeMachine = await this.prisma.typeMachine.findUnique({
where: { id: typeMachineId },
@@ -108,25 +139,40 @@ export class MachinesService {
componentSelections: MachineComponentSelectionDto[],
pieceSelections: MachinePieceSelectionDto[],
) {
const componentRequirements = (Array.isArray(typeMachine.componentRequirements)
? typeMachine.componentRequirements
: []) as any[];
const pieceRequirements = (Array.isArray(typeMachine.pieceRequirements)
? typeMachine.pieceRequirements
: []) as any[];
const componentRequirements = (
Array.isArray(typeMachine.componentRequirements)
? typeMachine.componentRequirements
: []
) as any[];
const pieceRequirements = (
Array.isArray(typeMachine.pieceRequirements)
? typeMachine.pieceRequirements
: []
) as any[];
const componentRequirementMap = new Map(
componentRequirements.map((requirement: any) => [requirement.id, requirement]),
componentRequirements.map((requirement: any) => [
requirement.id,
requirement,
]),
);
const pieceRequirementMap = new Map(
pieceRequirements.map((requirement: any) => [requirement.id, requirement]),
pieceRequirements.map((requirement: any) => [
requirement.id,
requirement,
]),
);
const componentSelectionMap = new Map<string, MachineComponentSelectionDto[]>();
const componentSelectionMap = new Map<
string,
MachineComponentSelectionDto[]
>();
for (const selection of componentSelections) {
const requirement = componentRequirementMap.get(selection.requirementId);
if (!requirement) {
throw new Error(`Sélection de composant invalide: requirementId=${selection.requirementId}`);
throw new Error(
`Sélection de composant invalide: requirementId=${selection.requirementId}`,
);
}
if (!componentSelectionMap.has(requirement.id)) {
componentSelectionMap.set(requirement.id, []);
@@ -138,7 +184,16 @@ export class MachinesService {
for (const selection of pieceSelections) {
const requirement = pieceRequirementMap.get(selection.requirementId);
if (!requirement) {
throw new Error(`Sélection de pièce invalide: requirementId=${selection.requirementId}`);
throw new Error(
`Sélection de pièce invalide: requirementId=${selection.requirementId}`,
);
}
if (!selection.pieceModelId) {
throw new Error(
`Le groupe de pièces "${
requirement.label || requirement.typePiece?.name || requirement.id
}" nécessite la sélection d'un modèle de pièce.`,
);
}
if (!pieceSelectionMap.has(requirement.id)) {
pieceSelectionMap.set(requirement.id, []);
@@ -147,24 +202,36 @@ export class MachinesService {
}
const componentModelIds = Array.from(
new Set(componentSelections.map((selection) => selection.componentModelId).filter(Boolean)),
new Set(
componentSelections
.map((selection) => selection.componentModelId)
.filter(Boolean),
),
) as string[];
const componentModels = componentModelIds.length
? await this.prisma.composantModel.findMany({
where: { id: { in: componentModelIds } },
})
: [];
const componentModelMap = new Map(componentModels.map((model) => [model.id, model]));
const componentModelMap = new Map(
componentModels.map((model) => [model.id, model]),
);
const pieceModelIds = Array.from(
new Set(pieceSelections.map((selection) => selection.pieceModelId).filter(Boolean)),
new Set(
pieceSelections
.map((selection) => selection.pieceModelId)
.filter(Boolean),
),
) as string[];
const pieceModels = pieceModelIds.length
? await this.prisma.pieceModel.findMany({
where: { id: { in: pieceModelIds } },
})
: [];
const pieceModelMap = new Map(pieceModels.map((model) => [model.id, model]));
const pieceModelMap = new Map(
pieceModels.map((model) => [model.id, model]),
);
for (const requirement of componentRequirements) {
const selections = componentSelectionMap.get(requirement.id) ?? [];
@@ -184,7 +251,9 @@ export class MachinesService {
}
if (!requirement.allowNewModels) {
const missingModel = selections.find((selection) => !selection.componentModelId);
const missingModel = selections.find(
(selection) => !selection.componentModelId,
);
if (missingModel) {
throw new Error(
`Le groupe de composants "${requirement.label || requirement.typeComposant?.name || requirement.id}" n'autorise que la sélection de modèles existants.`,
@@ -210,14 +279,6 @@ export class MachinesService {
);
}
if (!requirement.allowNewModels) {
const missingModel = selections.find((selection) => !selection.pieceModelId);
if (missingModel) {
throw new Error(
`Le groupe de pièces "${requirement.label || requirement.typePiece?.name || requirement.id}" n'autorise que la sélection de modèles existants.`,
);
}
}
}
for (const selection of componentSelections) {
@@ -226,11 +287,15 @@ export class MachinesService {
}
const model = componentModelMap.get(selection.componentModelId);
if (!model) {
throw new Error(`Modèle de composant introuvable: ${selection.componentModelId}`);
throw new Error(
`Modèle de composant introuvable: ${selection.componentModelId}`,
);
}
const requirement = componentRequirementMap.get(selection.requirementId);
if (!requirement) {
throw new Error(`Requirement de composant introuvable: ${selection.requirementId}`);
throw new Error(
`Requirement de composant introuvable: ${selection.requirementId}`,
);
}
if (model.typeComposantId !== requirement.typeComposantId) {
throw new Error(
@@ -240,16 +305,17 @@ export class MachinesService {
}
for (const selection of pieceSelections) {
if (!selection.pieceModelId) {
continue;
}
const model = pieceModelMap.get(selection.pieceModelId);
if (!model) {
throw new Error(`Modèle de pièce introuvable: ${selection.pieceModelId}`);
throw new Error(
`Modèle de pièce introuvable: ${selection.pieceModelId}`,
);
}
const requirement = pieceRequirementMap.get(selection.requirementId);
if (!requirement) {
throw new Error(`Requirement de pièce introuvable: ${selection.requirementId}`);
throw new Error(
`Requirement de pièce introuvable: ${selection.requirementId}`,
);
}
if (model.typePieceId !== requirement.typePieceId) {
throw new Error(
@@ -274,24 +340,36 @@ export class MachinesService {
} = createMachineDto;
if (!machineData.typeMachineId) {
throw new Error('typeMachineId est requis pour créer une machine à partir d\'un squelette.');
throw new Error(
"typeMachineId est requis pour créer une machine à partir d'un squelette.",
);
}
const typeMachine = await this.getTypeMachineConfiguration(machineData.typeMachineId);
const typeMachine = await this.getTypeMachineConfiguration(
machineData.typeMachineId,
);
const {
componentSelectionMap,
pieceSelectionMap,
componentModelMap,
pieceModelMap,
} = await this.buildConfigurationContext(typeMachine, componentSelections, pieceSelections);
} = await this.buildConfigurationContext(
typeMachine,
componentSelections,
pieceSelections,
);
const componentRequirements = (Array.isArray(typeMachine.componentRequirements)
? typeMachine.componentRequirements
: []) as any[];
const pieceRequirements = (Array.isArray(typeMachine.pieceRequirements)
? typeMachine.pieceRequirements
: []) as any[];
const componentRequirements = (
Array.isArray(typeMachine.componentRequirements)
? typeMachine.componentRequirements
: []
) as any[];
const pieceRequirements = (
Array.isArray(typeMachine.pieceRequirements)
? typeMachine.pieceRequirements
: []
) as any[];
return this.prisma.$transaction(async (prisma) => {
const machine = await prisma.machine.create({
@@ -307,15 +385,27 @@ export class MachinesService {
for (const requirement of componentRequirements) {
const selections = componentSelectionMap.get(requirement.id) ?? [];
for (const selection of selections) {
const model = selection.componentModelId ? componentModelMap.get(selection.componentModelId) : undefined;
const definition = this.normalizeComponentSelection(selection, requirement, model);
await this.createComponentsFromType(prisma, machine.id, [definition]);
const model = selection.componentModelId
? componentModelMap.get(selection.componentModelId)
: undefined;
const definition = this.normalizeComponentSelection(
selection,
requirement,
model,
);
await this.createComponentsFromType(prisma, machine.id, [
definition,
]);
}
}
} else {
const legacyComponents = (typeMachine as any).components;
if (legacyComponents) {
await this.createComponentsFromType(prisma, machine.id, legacyComponents);
await this.createComponentsFromType(
prisma,
machine.id,
legacyComponents,
);
}
}
@@ -323,20 +413,36 @@ export class MachinesService {
for (const requirement of pieceRequirements) {
const selections = pieceSelectionMap.get(requirement.id) ?? [];
for (const selection of selections) {
const model = selection.pieceModelId ? pieceModelMap.get(selection.pieceModelId) : undefined;
const definition = this.normalizePieceSelection(selection, requirement, model);
await this.createMachinePiecesFromType(prisma, machine.id, [definition]);
const model = selection.pieceModelId
? pieceModelMap.get(selection.pieceModelId)
: undefined;
const definition = this.normalizePieceSelection(
selection,
requirement,
model,
);
await this.createMachinePiecesFromType(prisma, machine.id, [
definition,
]);
}
}
} else {
const legacyPieces = (typeMachine as any).machinePieces;
if (legacyPieces) {
await this.createMachinePiecesFromType(prisma, machine.id, legacyPieces);
await this.createMachinePiecesFromType(
prisma,
machine.id,
legacyPieces,
);
}
}
if (typeMachine.customFields && typeMachine.customFields.length > 0) {
await this.createMachineCustomFieldsFromType(prisma, machine.id, typeMachine.customFields);
await this.createMachineCustomFieldsFromType(
prisma,
machine.id,
typeMachine.customFields,
);
}
return prisma.machine.findUnique({
@@ -371,28 +477,43 @@ export class MachinesService {
requirement: any,
model?: any,
): any {
const baseDefinition = selection.definition ?? (model?.structure ?? {});
const baseDefinition = selection.definition ?? model?.structure ?? {};
const definition = this.cloneStructure(baseDefinition);
const prepared: any = definition && typeof definition === 'object' && !Array.isArray(definition) ? definition : {};
const prepared: any =
definition && typeof definition === 'object' && !Array.isArray(definition)
? definition
: {};
prepared.name = prepared.name || model?.name || requirement?.typeComposant?.name || 'Composant';
prepared.reference = prepared.reference ?? model?.structure?.reference ?? '';
prepared.emplacement = prepared.emplacement ?? model?.structure?.emplacement ?? '';
prepared.name =
prepared.name ||
model?.name ||
requirement?.typeComposant?.name ||
'Composant';
prepared.reference =
prepared.reference ?? model?.structure?.reference ?? '';
prepared.emplacement =
prepared.emplacement ?? model?.structure?.emplacement ?? '';
prepared.prix = prepared.prix ?? model?.structure?.prix ?? null;
prepared.customFields = Array.isArray(prepared.customFields) ? prepared.customFields : [];
prepared.customFields = Array.isArray(prepared.customFields)
? prepared.customFields
: [];
prepared.pieces = Array.isArray(prepared.pieces)
? prepared.pieces
: prepared.pieces
? [prepared.pieces]
: [];
? [prepared.pieces]
: [];
prepared.subComponents = Array.isArray(prepared.subComponents)
? prepared.subComponents
: prepared.subComponents
? [prepared.subComponents]
: [];
? [prepared.subComponents]
: [];
prepared.typeComposantId = prepared.typeComposantId || requirement?.typeComposantId || model?.typeComposantId || null;
prepared.typeComposantId =
prepared.typeComposantId ||
requirement?.typeComposantId ||
model?.typeComposantId ||
null;
prepared.__componentModelId = selection.componentModelId ?? null;
prepared.__requirementId = requirement?.id ?? null;
@@ -404,43 +525,80 @@ export class MachinesService {
requirement: any,
model?: any,
): any {
const baseDefinition = selection.definition ?? (model?.structure ?? {});
const definition = this.cloneStructure(baseDefinition);
const prepared: any = definition && typeof definition === 'object' && !Array.isArray(definition) ? definition : {};
if (!model) {
throw new Error(
`Modèle de pièce introuvable: ${selection.pieceModelId}`,
);
}
prepared.name = prepared.name || model?.name || requirement?.typePiece?.name || 'Pièce';
prepared.customFields = Array.isArray(prepared.customFields) ? prepared.customFields : [];
prepared.typePieceId = prepared.typePieceId || requirement?.typePieceId || model?.typePieceId || null;
prepared.__pieceModelId = selection.pieceModelId ?? null;
const baseDefinition = model.structure ?? {};
const definition = this.cloneStructure(baseDefinition);
const prepared: any =
definition && typeof definition === 'object' && !Array.isArray(definition)
? definition
: {};
prepared.name = prepared.name || model.name || 'Pièce';
prepared.customFields = Array.isArray(prepared.customFields)
? prepared.customFields
: [];
prepared.typePieceId =
prepared.typePieceId ||
model.typePieceId ||
requirement?.typePieceId ||
null;
prepared.__pieceModelId = selection.pieceModelId;
prepared.__requirementId = requirement?.id ?? null;
return prepared;
}
private async createComponentsFromType(prisma: any, machineId: string, components: any[], parentComposantId?: string) {
private async createComponentsFromType(
prisma: any,
machineId: string,
components: any[],
parentComposantId?: string,
) {
for (const component of components) {
if (!component || !component.name) continue;
const customFields = Array.isArray(component.customFields) ? component.customFields : [];
const componentPieces = Array.isArray(component.pieces) ? component.pieces : [];
const subComponents = Array.isArray(component.subComponents) ? component.subComponents : [];
const customFields = Array.isArray(component.customFields)
? component.customFields
: [];
const componentPieces = Array.isArray(component.pieces)
? component.pieces
: [];
const subComponents = Array.isArray(component.subComponents)
? component.subComponents
: [];
const componentModelId = component.__componentModelId ?? null;
const requirementId = component.__requirementId ?? null;
const providedTypeComposantId = component.typeComposantId
?? (component.typeComposant && component.typeComposant.id ? component.typeComposant.id : null);
const providedTypeComposantId =
component.typeComposantId ??
(component.typeComposant && component.typeComposant.id
? component.typeComposant.id
: null);
let typeComposantId: string | null = providedTypeComposantId ?? null;
if (!typeComposantId && customFields.length > 0) {
let typeComposant = await prisma.typeComposant.findFirst({
where: { name: component.name },
let typeComposant = await prisma.modelType.findFirst({
where: {
name: component.name,
category: ModelCategory.COMPONENT,
},
});
if (!typeComposant) {
typeComposant = await prisma.typeComposant.create({
typeComposant = await prisma.modelType.create({
data: {
name: component.name,
code: await this.generateUniqueComponentTypeCode(
prisma,
component.name,
),
category: ModelCategory.COMPONENT,
description: component.description || '',
},
});
@@ -466,7 +624,10 @@ export class MachinesService {
data: {
name: component.name,
reference: component.reference || '',
constructeurId: await this.resolveConstructeurId(prisma, component.constructeur),
constructeurId: await this.resolveConstructeurId(
prisma,
component.constructeur,
),
emplacement: component.emplacement || '',
prix: component.prix ?? null,
machineId,
@@ -483,7 +644,9 @@ export class MachinesService {
});
for (const customField of typeCustomFields) {
const defaultValue = customFields.find((cf) => cf.name === customField.name)?.defaultValue || '';
const defaultValue =
customFields.find((cf) => cf.name === customField.name)
?.defaultValue || '';
await prisma.customFieldValue.create({
data: {
value: defaultValue,
@@ -497,11 +660,14 @@ export class MachinesService {
for (const piece of componentPieces) {
if (!piece || !piece.name) continue;
const pieceCustomFields = Array.isArray(piece.customFields) ? piece.customFields : [];
const pieceCustomFields = Array.isArray(piece.customFields)
? piece.customFields
: [];
const pieceModelId = piece.__pieceModelId ?? null;
const pieceRequirementId = piece.__requirementId ?? null;
const providedTypePieceId = piece.typePieceId
?? (piece.typePiece && piece.typePiece.id ? piece.typePiece.id : null);
const providedTypePieceId =
piece.typePieceId ??
(piece.typePiece && piece.typePiece.id ? piece.typePiece.id : null);
let typePieceId: string | null = providedTypePieceId ?? null;
@@ -539,7 +705,10 @@ export class MachinesService {
data: {
name: piece.name,
reference: piece.reference || '',
constructeurId: await this.resolveConstructeurId(prisma, piece.constructeur),
constructeurId: await this.resolveConstructeurId(
prisma,
piece.constructeur,
),
emplacement: piece.emplacement || '',
prix: piece.prix ?? null,
composantId: createdComposant.id,
@@ -555,7 +724,9 @@ export class MachinesService {
});
for (const customField of typePieceCustomFields) {
const defaultValue = pieceCustomFields.find((cf) => cf.name === customField.name)?.defaultValue || '';
const defaultValue =
pieceCustomFields.find((cf) => cf.name === customField.name)
?.defaultValue || '';
await prisma.customFieldValue.create({
data: {
value: defaultValue,
@@ -568,20 +739,32 @@ export class MachinesService {
}
if (subComponents.length > 0) {
await this.createComponentsFromType(prisma, machineId, subComponents, createdComposant.id);
await this.createComponentsFromType(
prisma,
machineId,
subComponents,
createdComposant.id,
);
}
}
}
private async createMachinePiecesFromType(prisma: any, machineId: string, machinePieces: any[]) {
private async createMachinePiecesFromType(
prisma: any,
machineId: string,
machinePieces: any[],
) {
for (const piece of machinePieces) {
if (!piece || !piece.name) continue;
const customFields = Array.isArray(piece.customFields) ? piece.customFields : [];
const customFields = Array.isArray(piece.customFields)
? piece.customFields
: [];
const pieceModelId = piece.__pieceModelId ?? null;
const requirementId = piece.__requirementId ?? null;
const providedTypePieceId = piece.typePieceId
?? (piece.typePiece && piece.typePiece.id ? piece.typePiece.id : null);
const providedTypePieceId =
piece.typePieceId ??
(piece.typePiece && piece.typePiece.id ? piece.typePiece.id : null);
let typePieceId: string | null = providedTypePieceId ?? null;
@@ -619,7 +802,10 @@ export class MachinesService {
data: {
name: piece.name,
reference: piece.reference || '',
constructeurId: await this.resolveConstructeurId(prisma, piece.constructeur),
constructeurId: await this.resolveConstructeurId(
prisma,
piece.constructeur,
),
emplacement: piece.emplacement || '',
prix: piece.prix ?? null,
machineId,
@@ -635,7 +821,9 @@ export class MachinesService {
});
for (const customField of typePieceCustomFields) {
const defaultValue = customFields.find((cf) => cf.name === customField.name)?.defaultValue || '';
const defaultValue =
customFields.find((cf) => cf.name === customField.name)
?.defaultValue || '';
await prisma.customFieldValue.create({
data: {
value: defaultValue,
@@ -669,10 +857,14 @@ export class MachinesService {
}
}
private async createMachineCustomFieldsFromType(prisma: any, machineId: string, machineCustomFields: any[]) {
private async createMachineCustomFieldsFromType(
prisma: any,
machineId: string,
machineCustomFields: any[],
) {
for (const customField of machineCustomFields) {
if (!customField || !customField.name) continue;
// Créer le champ personnalisé pour la machine
const createdCustomField = await prisma.customField.create({
data: {
@@ -710,10 +902,8 @@ export class MachinesService {
}
async reconfigure(id: string, reconfigureMachineDto: ReconfigureMachineDto) {
const {
componentSelections = [],
pieceSelections = [],
} = reconfigureMachineDto;
const { componentSelections = [], pieceSelections = [] } =
reconfigureMachineDto;
const machine = await this.prisma.machine.findUnique({
where: { id },
@@ -729,7 +919,9 @@ export class MachinesService {
}
if (!machine.typeMachineId || !machine.typeMachine) {
throw new Error('Impossible de reconfigurer une machine sans type de machine associé.');
throw new Error(
'Impossible de reconfigurer une machine sans type de machine associé.',
);
}
const typeMachine = machine.typeMachine;
@@ -739,14 +931,22 @@ export class MachinesService {
pieceSelectionMap,
componentModelMap,
pieceModelMap,
} = await this.buildConfigurationContext(typeMachine, componentSelections, pieceSelections);
} = await this.buildConfigurationContext(
typeMachine,
componentSelections,
pieceSelections,
);
const componentRequirements = (Array.isArray(typeMachine.componentRequirements)
? typeMachine.componentRequirements
: []) as any[];
const pieceRequirements = (Array.isArray(typeMachine.pieceRequirements)
? typeMachine.pieceRequirements
: []) as any[];
const componentRequirements = (
Array.isArray(typeMachine.componentRequirements)
? typeMachine.componentRequirements
: []
) as any[];
const pieceRequirements = (
Array.isArray(typeMachine.pieceRequirements)
? typeMachine.pieceRequirements
: []
) as any[];
return this.prisma.$transaction(async (prisma) => {
await prisma.customFieldValue.deleteMany({
@@ -797,7 +997,11 @@ export class MachinesService {
const model = selection.componentModelId
? componentModelMap.get(selection.componentModelId)
: undefined;
const definition = this.normalizeComponentSelection(selection, requirement, model);
const definition = this.normalizeComponentSelection(
selection,
requirement,
model,
);
await this.createComponentsFromType(prisma, id, [definition]);
}
}
@@ -815,7 +1019,11 @@ export class MachinesService {
const model = selection.pieceModelId
? pieceModelMap.get(selection.pieceModelId)
: undefined;
const definition = this.normalizePieceSelection(selection, requirement, model);
const definition = this.normalizePieceSelection(
selection,
requirement,
model,
);
await this.createMachinePiecesFromType(prisma, id, [definition]);
}
}
@@ -842,9 +1050,9 @@ export class MachinesService {
}
private async resolveConstructeurId(prisma: any, rawName?: string) {
if (!rawName) return null
const name = String(rawName).trim()
if (!name) return null
if (!rawName) return null;
const name = String(rawName).trim();
if (!name) return null;
const existing = await prisma.constructeur.findFirst({
where: {
@@ -853,15 +1061,15 @@ export class MachinesService {
mode: 'insensitive',
},
},
})
});
if (existing) return existing.id
if (existing) return existing.id;
const created = await prisma.constructeur.create({
data: { name },
})
});
return created.id
return created.id;
}
async remove(id: string) {
@@ -983,17 +1191,31 @@ export class MachinesService {
// Traiter les composants existants
for (const component of machine.composants) {
const typeComponent = components.find((c: any) => c.name === component.name);
if (typeComponent && typeComponent.customFields && typeComponent.customFields.length > 0) {
const typeComponent = components.find(
(c: any) => c.name === component.name,
);
if (
typeComponent &&
typeComponent.customFields &&
typeComponent.customFields.length > 0
) {
// Créer le type de composant s'il n'existe pas
let typeComposant = await this.prisma.typeComposant.findFirst({
where: { name: component.name },
let typeComposant = await this.prisma.modelType.findFirst({
where: {
name: component.name,
category: ModelCategory.COMPONENT,
},
});
if (!typeComposant) {
typeComposant = await this.prisma.typeComposant.create({
typeComposant = await this.prisma.modelType.create({
data: {
name: component.name,
code: await this.generateUniqueComponentTypeCode(
this.prisma,
component.name,
),
category: ModelCategory.COMPONENT,
description: typeComponent.description || '',
},
});
@@ -1042,7 +1264,10 @@ export class MachinesService {
});
if (!existingValue) {
const defaultValue = typeComponent.customFields.find((cf: any) => cf.name === customField.name)?.defaultValue || '';
const defaultValue =
typeComponent.customFields.find(
(cf: any) => cf.name === customField.name,
)?.defaultValue || '';
await this.prisma.customFieldValue.create({
data: {
value: defaultValue,
@@ -1055,18 +1280,33 @@ export class MachinesService {
// Traiter les pièces du composant
for (const piece of component.pieces) {
const typePiece = typeComponent.pieces?.find((p: any) => p.name === piece.name);
if (typePiece && typePiece.customFields && typePiece.customFields.length > 0) {
const typePiece = typeComponent.pieces?.find(
(p: any) => p.name === piece.name,
);
if (
typePiece &&
typePiece.customFields &&
typePiece.customFields.length > 0
) {
// Créer le type de pièce s'il n'existe pas
let typePieceEntity = await this.prisma.typePiece.findFirst({
where: { name: piece.name },
let typePieceEntity = await this.prisma.modelType.findFirst({
where: {
name: piece.name,
category: ModelCategory.PIECE,
},
});
if (!typePieceEntity) {
typePieceEntity = await this.prisma.typePiece.create({
typePieceEntity = await this.prisma.modelType.create({
data: {
name: piece.name,
code: await this.generateUniqueComponentTypeCode(
this.prisma,
piece.name,
),
category: ModelCategory.PIECE,
description: typePiece.description || '',
notes: typePiece.description || '',
},
});
}
@@ -1106,15 +1346,19 @@ export class MachinesService {
});
for (const customField of customFields) {
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 defaultValue = typePiece.customFields.find((cf: any) => cf.name === customField.name)?.defaultValue || '';
const defaultValue =
typePiece.customFields.find(
(cf: any) => cf.name === customField.name,
)?.defaultValue || '';
await this.prisma.customFieldValue.create({
data: {
value: defaultValue,
@@ -1132,17 +1376,30 @@ export class MachinesService {
// Traiter les pièces de machine
for (const piece of machine.pieces) {
const typePiece = machinePieces.find((p: any) => p.name === piece.name);
if (typePiece && typePiece.customFields && typePiece.customFields.length > 0) {
if (
typePiece &&
typePiece.customFields &&
typePiece.customFields.length > 0
) {
// Créer le type de pièce s'il n'existe pas
let typePieceEntity = await this.prisma.typePiece.findFirst({
where: { name: piece.name },
let typePieceEntity = await this.prisma.modelType.findFirst({
where: {
name: piece.name,
category: ModelCategory.PIECE,
},
});
if (!typePieceEntity) {
typePieceEntity = await this.prisma.typePiece.create({
typePieceEntity = await this.prisma.modelType.create({
data: {
name: piece.name,
code: await this.generateUniqueComponentTypeCode(
this.prisma,
piece.name,
),
category: ModelCategory.PIECE,
description: typePiece.description || '',
notes: typePiece.description || '',
},
});
}
@@ -1190,7 +1447,10 @@ export class MachinesService {
});
if (!existingValue) {
const defaultValue = typePiece.customFields.find((cf: any) => cf.name === customField.name)?.defaultValue || '';
const defaultValue =
typePiece.customFields.find(
(cf: any) => cf.name === customField.name,
)?.defaultValue || '';
await this.prisma.customFieldValue.create({
data: {
value: defaultValue,

View File

@@ -0,0 +1,28 @@
import { IsEnum, IsOptional, IsString, Length, Matches } from 'class-validator';
export enum ModelCategory {
COMPONENT = 'COMPONENT',
PIECE = 'PIECE',
}
export class CreateModelTypeDto {
@IsString()
@Length(2, 120)
name!: string;
@IsString()
@Length(2, 60)
@Matches(/^[a-z0-9\-_.]+$/i)
code!: string;
@IsEnum(ModelCategory)
category!: ModelCategory;
@IsOptional()
@IsString()
notes?: string;
@IsOptional()
@IsString()
description?: string;
}

View File

@@ -0,0 +1,5 @@
import { PartialType } from '@nestjs/mapped-types';
import { CreateModelTypeDto } from './create-model-type.dto';
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
export class UpdateModelTypeDto extends PartialType(CreateModelTypeDto) {}

View File

@@ -0,0 +1,80 @@
import {
Body,
Controller,
Delete,
Get,
Header,
HttpCode,
HttpStatus,
Param,
Patch,
Post,
Query,
} from '@nestjs/common';
import { CreateModelTypeDto, ModelCategory } from './dto/create-model-type.dto';
import { UpdateModelTypeDto } from './dto/update-model-type.dto';
import { ModelTypeService } from './model-type.service';
@Controller('api/model-types')
export class ModelTypeController {
constructor(private readonly modelTypeService: ModelTypeService) {}
@Get()
list(
@Query('q') q?: string,
@Query('category') category?: string,
@Query('sort') sort?: 'name' | 'code' | 'createdAt',
@Query('dir') dir?: 'asc' | 'desc',
@Query('limit') limit?: string,
@Query('offset') offset?: string,
) {
return this.modelTypeService.list({
q,
category: this.normalizeCategory(category),
sort,
dir,
limit: this.parsePositiveNumber(limit),
offset: this.parsePositiveNumber(offset),
});
}
@Post()
@Header('Cache-Control', 'no-store')
create(@Body() dto: CreateModelTypeDto) {
return this.modelTypeService.create(dto);
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.modelTypeService.findOne(id);
}
@Patch(':id')
@Header('Cache-Control', 'no-store')
update(@Param('id') id: string, @Body() dto: UpdateModelTypeDto) {
return this.modelTypeService.update(id, dto);
}
@Delete(':id')
@Header('Cache-Control', 'no-store')
@HttpCode(HttpStatus.NO_CONTENT)
async remove(@Param('id') id: string) {
await this.modelTypeService.remove(id);
}
private normalizeCategory(value?: string) {
if (!value) return undefined;
return Object.values(ModelCategory).includes(value as ModelCategory)
? (value as ModelCategory)
: undefined;
}
private parsePositiveNumber(value?: string) {
if (value === undefined) return undefined;
const parsed = Number.parseInt(value, 10);
if (Number.isNaN(parsed) || parsed < 0) {
return undefined;
}
return parsed;
}
}

View File

@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { ModelTypeController } from './model-type.controller';
import { ModelTypeService } from './model-type.service';
@Module({
controllers: [ModelTypeController],
providers: [ModelTypeService],
})
export class ModelTypeModule {}

View File

@@ -0,0 +1,159 @@
import {
ConflictException,
Injectable,
NotFoundException,
} from '@nestjs/common';
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';
type SortField = 'name' | 'code' | 'createdAt';
type SortDirection = 'asc' | 'desc';
interface ListParams {
q?: string;
category?: ModelCategory;
sort?: SortField;
dir?: SortDirection;
limit?: number;
offset?: number;
}
@Injectable()
export class ModelTypeService {
private readonly allowedSortFields: SortField[] = [
'name',
'code',
'createdAt',
];
constructor(private readonly prisma: PrismaService) {}
async list(params: ListParams): Promise<{
items: PrismaModelType[];
total: number;
offset: number;
limit: number;
}> {
const {
q,
category,
sort = 'createdAt',
dir = 'desc',
limit = 20,
offset = 0,
} = params;
const cappedLimit = Math.min(Math.max(limit ?? 20, 1), 100);
const safeOffset = Math.max(offset ?? 0, 0);
const orderByField = this.allowedSortFields.includes(sort)
? sort
: 'createdAt';
const orderByDir: SortDirection = dir === 'asc' ? 'asc' : 'desc';
const where: Prisma.ModelTypeWhereInput = {};
if (category) {
where.category = category;
}
if (q?.trim()) {
const term = q.trim();
where.OR = [
{ name: { contains: term, mode: 'insensitive' } },
{ code: { contains: term, mode: 'insensitive' } },
];
}
const [items, total] = await this.prisma.$transaction([
this.prisma.modelType.findMany({
where,
orderBy: { [orderByField]: orderByDir },
skip: safeOffset,
take: cappedLimit,
}),
this.prisma.modelType.count({ where }),
]);
return {
items,
total,
offset: safeOffset,
limit: cappedLimit,
};
}
async create(dto: CreateModelTypeDto): Promise<PrismaModelType> {
try {
return await this.prisma.modelType.create({
data: {
name: dto.name,
code: dto.code,
category: dto.category,
notes: dto.notes,
description: dto.description ?? null,
},
});
} catch (error) {
this.handlePrismaError(error);
}
}
async update(id: string, dto: UpdateModelTypeDto): Promise<PrismaModelType> {
try {
return await this.prisma.modelType.update({
where: { id },
data: {
...dto,
description:
dto.description === undefined ? undefined : dto.description ?? null,
},
});
} catch (error) {
this.handlePrismaError(error);
}
}
async remove(id: string): Promise<void> {
try {
await this.prisma.modelType.delete({ where: { id } });
} catch (error) {
this.handlePrismaError(error);
}
}
async findOne(id: string): Promise<PrismaModelType> {
const modelType = await this.prisma.modelType.findUnique({ where: { id } });
if (!modelType) {
throw new NotFoundException('Type de modèle introuvable.');
}
return modelType;
}
private handlePrismaError(error: unknown): never {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
if (error.code === 'P2002' && this.isUniqueCodeConstraint(error)) {
throw new ConflictException('Ce code est déjà utilisé.');
}
if (error.code === 'P2025') {
throw new NotFoundException('Type de modèle introuvable.');
}
}
throw error;
}
private isUniqueCodeConstraint(error: Prisma.PrismaClientKnownRequestError) {
const { target } = error.meta ?? {};
if (Array.isArray(target)) {
return target.includes('code');
}
if (typeof target === 'string') {
return target === 'code';
}
return false;
}
}

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 { PiecesService } from './pieces.service';
import { CreatePieceDto, UpdatePieceDto } from '../shared/dto/piece.dto';

View File

@@ -4,6 +4,6 @@ import { PiecesService } from './pieces.service';
@Module({
controllers: [PiecesController],
providers: [PiecesService]
providers: [PiecesService],
})
export class PiecesModule {}

View File

@@ -43,9 +43,7 @@ describe('PiecesService', () => {
prisma.machine.findUnique.mockResolvedValue({
id: 'machine-1',
typeMachine: {
pieceRequirements: [
{ id: 'req-1', typePieceId: 'type-piece-1' },
],
pieceRequirements: [{ id: 'req-1', typePieceId: 'type-piece-1' }],
},
});
@@ -70,9 +68,7 @@ describe('PiecesService', () => {
prisma.machine.findUnique.mockResolvedValue({
id: 'machine-1',
typeMachine: {
pieceRequirements: [
{ id: 'req-1', typePieceId: 'type-piece-1' },
],
pieceRequirements: [{ id: 'req-1', typePieceId: 'type-piece-1' }],
},
});

View File

@@ -180,9 +180,7 @@ export class PiecesService {
});
if (!composant) {
throw new BadRequestException(
'Le composant spécifié est introuvable.',
);
throw new BadRequestException('Le composant spécifié est introuvable.');
}
if (composant.machineId) {

View File

@@ -6,4 +6,4 @@ import { PrismaService } from './prisma.service';
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}
export class PrismaModule {}

View File

@@ -2,7 +2,10 @@ import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
export class PrismaService
extends PrismaClient
implements OnModuleInit, OnModuleDestroy
{
constructor() {
super({
log: ['query', 'info', 'warn', 'error'],
@@ -16,4 +19,4 @@ export class PrismaService extends PrismaClient implements OnModuleInit, OnModul
async onModuleDestroy() {
await this.$disconnect();
}
}
}

View File

@@ -1,6 +1,6 @@
import { Body, Controller, Delete, Get, Param, Post } from '@nestjs/common'
import { ProfilesService } from './profiles.service'
import { CreateProfileDto } from '../shared/dto/profile.dto'
import { Body, Controller, Delete, Get, Param, Post } from '@nestjs/common';
import { ProfilesService } from './profiles.service';
import { CreateProfileDto } from '../shared/dto/profile.dto';
@Controller('profiles')
export class ProfilesController {
@@ -8,16 +8,16 @@ export class ProfilesController {
@Get()
async findAll() {
return this.profilesService.findAllActive()
return this.profilesService.findAllActive();
}
@Post()
async create(@Body() dto: CreateProfileDto) {
return this.profilesService.create(dto)
return this.profilesService.create(dto);
}
@Delete(':id')
async delete(@Param('id') id: string) {
return this.profilesService.deactivate(id)
return this.profilesService.deactivate(id);
}
}

View File

@@ -1,6 +1,6 @@
import { Module } from '@nestjs/common'
import { ProfilesController } from './profiles.controller'
import { ProfilesService } from './profiles.service'
import { Module } from '@nestjs/common';
import { ProfilesController } from './profiles.controller';
import { ProfilesService } from './profiles.service';
@Module({
controllers: [ProfilesController],

View File

@@ -1,13 +1,18 @@
import { Injectable, NotFoundException, BadRequestException, OnModuleInit } from '@nestjs/common'
import { PrismaService } from '../prisma/prisma.service'
import { CreateProfileDto } from '../shared/dto/profile.dto'
import {
Injectable,
NotFoundException,
BadRequestException,
OnModuleInit,
} from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { CreateProfileDto } from '../shared/dto/profile.dto';
@Injectable()
export class ProfilesService implements OnModuleInit {
constructor(private readonly prisma: PrismaService) {}
async onModuleInit() {
await this.ensureDefaultProfile()
await this.ensureDefaultProfile();
}
async findAllActive() {
@@ -21,11 +26,11 @@ export class ProfilesService implements OnModuleInit {
createdAt: true,
updatedAt: true,
},
})
});
}
async findActiveById(profileId: string) {
if (!profileId) return null
if (!profileId) return null;
return this.prisma.profile.findFirst({
where: {
@@ -40,15 +45,15 @@ export class ProfilesService implements OnModuleInit {
updatedAt: true,
isActive: true,
},
})
});
}
async create(dto: CreateProfileDto) {
const firstName = dto.firstName.trim()
const lastName = dto.lastName.trim()
const firstName = dto.firstName.trim();
const lastName = dto.lastName.trim();
if (!firstName || !lastName) {
throw new BadRequestException('Le prénom et le nom sont obligatoires.')
throw new BadRequestException('Le prénom et le nom sont obligatoires.');
}
return this.prisma.profile.create({
@@ -64,13 +69,15 @@ export class ProfilesService implements OnModuleInit {
createdAt: true,
updatedAt: true,
},
})
});
}
async deactivate(profileId: string) {
const existing = await this.prisma.profile.findUnique({ where: { id: profileId } })
const existing = await this.prisma.profile.findUnique({
where: { id: profileId },
});
if (!existing) {
throw new NotFoundException('Profil introuvable')
throw new NotFoundException('Profil introuvable');
}
if (!existing.isActive) {
@@ -84,7 +91,7 @@ export class ProfilesService implements OnModuleInit {
createdAt: true,
updatedAt: true,
},
})
});
}
return this.prisma.profile.update({
@@ -98,27 +105,34 @@ export class ProfilesService implements OnModuleInit {
createdAt: true,
updatedAt: true,
},
})
});
}
private async ensureDefaultProfile() {
const count = await this.prisma.profile.count({ where: { isActive: true } }).catch((err) => {
console.error('Failed to count profiles during ensureDefaultProfile:', err.message)
return 0
})
if (count > 0) return
const count = await this.prisma.profile
.count({ where: { isActive: true } })
.catch((err) => {
console.error(
'Failed to count profiles during ensureDefaultProfile:',
err.message,
);
return 0;
});
if (count > 0) return;
const firstName = process.env.DEFAULT_PROFILE_FIRST_NAME?.trim() || 'Admin'
const lastName = process.env.DEFAULT_PROFILE_LAST_NAME?.trim() || 'Général'
const firstName = process.env.DEFAULT_PROFILE_FIRST_NAME?.trim() || 'Admin';
const lastName = process.env.DEFAULT_PROFILE_LAST_NAME?.trim() || 'Général';
await this.prisma.profile.create({
data: {
firstName,
lastName,
isActive: true,
},
}).catch((err) => {
console.error('Failed to create default profile:', err.message)
})
await this.prisma.profile
.create({
data: {
firstName,
lastName,
isActive: true,
},
})
.catch((err) => {
console.error('Failed to create default profile:', err.message);
});
}
}

View File

@@ -7,10 +7,10 @@ import {
Post,
Req,
UnauthorizedException,
} from '@nestjs/common'
import { Request } from 'express'
import { ProfilesService } from '../profiles/profiles.service'
import { ActivateProfileDto } from '../shared/dto/profile.dto'
} from '@nestjs/common';
import { Request } from 'express';
import { ProfilesService } from '../profiles/profiles.service';
import { ActivateProfileDto } from '../shared/dto/profile.dto';
@Controller('session/profile')
export class ProfileSessionController {
@@ -19,46 +19,50 @@ export class ProfileSessionController {
@Get()
async getActiveProfile(@Req() req: Request) {
if (!req.session?.profileId) {
throw new UnauthorizedException('Aucun profil actif.')
throw new UnauthorizedException('Aucun profil actif.');
}
const profile = await this.profilesService.findActiveById(req.session.profileId)
const profile = await this.profilesService.findActiveById(
req.session.profileId,
);
if (!profile) {
req.session.profileId = undefined
throw new UnauthorizedException('Profil introuvable ou inactif.')
req.session.profileId = undefined;
throw new UnauthorizedException('Profil introuvable ou inactif.');
}
return profile
return profile;
}
@Post()
async activateProfile(@Req() req: Request, @Body() dto: ActivateProfileDto) {
if (!dto.profileId) {
throw new BadRequestException('profileId est requis.')
throw new BadRequestException('profileId est requis.');
}
const profile = await this.profilesService.findActiveById(dto.profileId)
const profile = await this.profilesService.findActiveById(dto.profileId);
if (!profile) {
throw new UnauthorizedException('Profil introuvable ou inactif.')
throw new UnauthorizedException('Profil introuvable ou inactif.');
}
req.session.profileId = profile.id
return profile
req.session.profileId = profile.id;
return profile;
}
@Delete()
async logout(@Req() req: Request) {
if (!req.session) {
return { success: true }
return { success: true };
}
return new Promise((resolve, reject) => {
req.session.destroy((err) => {
if (err) {
return reject(new BadRequestException('Impossible de déconnecter la session.'))
return reject(
new BadRequestException('Impossible de déconnecter la session.'),
);
}
resolve({ success: true })
})
})
resolve({ success: true });
});
});
}
}

View File

@@ -1,6 +1,6 @@
import { Module } from '@nestjs/common'
import { ProfileSessionController } from './session.controller'
import { ProfilesModule } from '../profiles/profiles.module'
import { Module } from '@nestjs/common';
import { ProfileSessionController } from './session.controller';
import { ProfilesModule } from '../profiles/profiles.module';
@Module({
imports: [ProfilesModule],

View File

@@ -22,7 +22,7 @@ export class CreateComposantDto {
constructeurId?: string;
@IsOptional()
@Transform(({ value }) => value === '' ? null : value)
@Transform(({ value }) => (value === '' ? null : value))
@IsNumber({}, { message: 'prix must be a valid number' })
prix?: number | null;
@@ -56,7 +56,7 @@ export class UpdateComposantDto {
constructeurId?: string;
@IsOptional()
@Transform(({ value }) => value === '' ? null : value)
@Transform(({ value }) => (value === '' ? null : value))
@IsNumber({}, { message: 'prix must be a valid number' })
prix?: number | null;

View File

@@ -1,28 +1,28 @@
import { IsEmail, IsOptional, IsString } from 'class-validator'
import { IsEmail, IsOptional, IsString } from 'class-validator';
export class CreateConstructeurDto {
@IsString()
name: string
name: string;
@IsOptional()
@IsEmail()
email?: string
email?: string;
@IsOptional()
@IsString()
phone?: string
phone?: string;
}
export class UpdateConstructeurDto {
@IsOptional()
@IsString()
name?: string
name?: string;
@IsOptional()
@IsEmail()
email?: string
email?: string;
@IsOptional()
@IsString()
phone?: string
phone?: string;
}

View File

@@ -58,4 +58,4 @@ export class UpdateCustomFieldValueDto {
@IsOptional()
@IsString()
value?: string;
}
}

View File

@@ -18,12 +18,8 @@ export class MachinePieceSelectionDto {
@IsString()
requirementId: string;
@IsOptional()
@IsString()
pieceModelId?: string;
@IsOptional()
definition?: any;
pieceModelId: string;
}
export class CreateMachineDto {

View File

@@ -22,7 +22,7 @@ export class CreatePieceDto {
constructeurId?: string;
@IsOptional()
@Transform(({ value }) => value === '' ? null : value)
@Transform(({ value }) => (value === '' ? null : value))
@IsNumber({}, { message: 'prix must be a valid number' })
prix?: number | null;
@@ -56,7 +56,7 @@ export class UpdatePieceDto {
constructeurId?: string;
@IsOptional()
@Transform(({ value }) => value === '' ? null : value)
@Transform(({ value }) => (value === '' ? null : value))
@IsNumber({}, { message: 'prix must be a valid number' })
prix?: number | null;

View File

@@ -1,19 +1,19 @@
import { IsNotEmpty, IsString, MaxLength } from 'class-validator'
import { IsNotEmpty, IsString, MaxLength } from 'class-validator';
export class CreateProfileDto {
@IsString()
@IsNotEmpty()
@MaxLength(100)
firstName!: string
firstName!: string;
@IsString()
@IsNotEmpty()
@MaxLength(100)
lastName!: string
lastName!: string;
}
export class ActivateProfileDto {
@IsString()
@IsNotEmpty()
profileId!: string
profileId!: string;
}

View File

@@ -1,4 +1,12 @@
import { IsString, IsOptional, IsArray, IsObject, IsBoolean, IsEnum, IsInt } from 'class-validator';
import {
IsString,
IsOptional,
IsArray,
IsObject,
IsBoolean,
IsEnum,
IsInt,
} from 'class-validator';
import { Type } from 'class-transformer';
import { ValidateNested } from 'class-validator';
@@ -7,7 +15,7 @@ export enum CustomFieldType {
NUMBER = 'number',
SELECT = 'select',
BOOLEAN = 'boolean',
DATE = 'date'
DATE = 'date',
}
export class CreateCustomFieldDto {

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 { SitesService } from './sites.service';
import { CreateSiteDto, UpdateSiteDto } from '../shared/dto/site.dto';

View File

@@ -4,6 +4,6 @@ import { SitesService } from './sites.service';
@Module({
controllers: [SitesController],
providers: [SitesService]
providers: [SitesService],
})
export class SitesModule {}

View File

@@ -70,10 +70,10 @@ export class SitesService {
});
if (machinesInSite.length > 0) {
const machineNames = machinesInSite.map(m => m.name).join(', ');
const machineNames = machinesInSite.map((m) => m.name).join(', ');
throw new Error(
`Impossible de supprimer ce site car il contient ${machinesInSite.length} machine(s): ${machineNames}. ` +
`Veuillez d'abord supprimer ou déplacer ces machines vers un autre site.`
`Veuillez d'abord supprimer ou déplacer ces machines vers un autre site.`,
);
}

View File

@@ -1,7 +1,7 @@
import 'express-session'
import 'express-session';
declare module 'express-session' {
interface SessionData {
profileId?: string
profileId?: string;
}
}

View File

@@ -1,7 +1,16 @@
import { Controller, Get, Post, Body, Patch, Param, Delete, Query } from '@nestjs/common';
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
Query,
} from '@nestjs/common';
import { TypesService } from './types.service';
import {
CreateTypeMachineDto,
import {
CreateTypeMachineDto,
UpdateTypeMachineDto,
CreateTypeComposantDto,
UpdateTypeComposantDto,
@@ -10,7 +19,7 @@ import {
CreateComposantModelDto,
UpdateComposantModelDto,
CreatePieceModelDto,
UpdatePieceModelDto
UpdatePieceModelDto,
} from '../shared/dto/type.dto';
@Controller('types')
@@ -34,7 +43,10 @@ export class TypesController {
}
@Patch('machines/:id')
updateTypeMachine(@Param('id') id: string, @Body() updateTypeMachineDto: UpdateTypeMachineDto) {
updateTypeMachine(
@Param('id') id: string,
@Body() updateTypeMachineDto: UpdateTypeMachineDto,
) {
return this.typesService.updateTypeMachine(id, updateTypeMachineDto);
}
@@ -56,7 +68,9 @@ export class TypesController {
// ComposantModel routes
@Post('composants/models')
createComposantModel(@Body() createComposantModelDto: CreateComposantModelDto) {
createComposantModel(
@Body() createComposantModelDto: CreateComposantModelDto,
) {
return this.typesService.createComposantModel(createComposantModelDto);
}
@@ -71,7 +85,10 @@ export class TypesController {
}
@Patch('composants/models/:id')
updateComposantModel(@Param('id') id: string, @Body() updateComposantModelDto: UpdateComposantModelDto) {
updateComposantModel(
@Param('id') id: string,
@Body() updateComposantModelDto: UpdateComposantModelDto,
) {
return this.typesService.updateComposantModel(id, updateComposantModelDto);
}
@@ -86,7 +103,10 @@ export class TypesController {
}
@Patch('composants/:id')
updateTypeComposant(@Param('id') id: string, @Body() updateTypeComposantDto: UpdateTypeComposantDto) {
updateTypeComposant(
@Param('id') id: string,
@Body() updateTypeComposantDto: UpdateTypeComposantDto,
) {
return this.typesService.updateTypeComposant(id, updateTypeComposantDto);
}
@@ -123,7 +143,10 @@ export class TypesController {
}
@Patch('pieces/models/:id')
updatePieceModel(@Param('id') id: string, @Body() updatePieceModelDto: UpdatePieceModelDto) {
updatePieceModel(
@Param('id') id: string,
@Body() updatePieceModelDto: UpdatePieceModelDto,
) {
return this.typesService.updatePieceModel(id, updatePieceModelDto);
}
@@ -138,7 +161,10 @@ export class TypesController {
}
@Patch('pieces/:id')
updateTypePiece(@Param('id') id: string, @Body() updateTypePieceDto: UpdateTypePieceDto) {
updateTypePiece(
@Param('id') id: string,
@Body() updateTypePieceDto: UpdateTypePieceDto,
) {
return this.typesService.updateTypePiece(id, updateTypePieceDto);
}

View File

@@ -4,6 +4,6 @@ import { TypesService } from './types.service';
@Module({
controllers: [TypesController],
providers: [TypesService]
providers: [TypesService],
})
export class TypesModule {}

View File

@@ -1,7 +1,8 @@
import { Injectable } from '@nestjs/common';
import { ModelCategory } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import {
CreateTypeMachineDto,
import {
CreateTypeMachineDto,
UpdateTypeMachineDto,
CreateTypeComposantDto,
UpdateTypeComposantDto,
@@ -10,53 +11,93 @@ import {
CreateComposantModelDto,
UpdateComposantModelDto,
CreatePieceModelDto,
UpdatePieceModelDto
UpdatePieceModelDto,
} from '../shared/dto/type.dto';
@Injectable()
export class TypesService {
constructor(private prisma: PrismaService) {}
private slugifyName(name: string): string {
return name
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
.replace(/-+/g, '-');
}
private async generateUniqueModelTypeCode(name: string): Promise<string> {
const base = this.slugifyName(name) || 'type';
let candidate = base;
let suffix = 1;
while (
await this.prisma.modelType.findUnique({
where: { code: candidate },
select: { id: true },
})
) {
candidate = `${base}-${suffix++}`;
}
return candidate;
}
// TypeMachine methods
async createTypeMachine(createTypeMachineDto: CreateTypeMachineDto) {
const { customFields, componentRequirements, pieceRequirements, ...typeData } = createTypeMachineDto;
const {
customFields,
componentRequirements,
pieceRequirements,
...typeData
} = createTypeMachineDto;
return this.prisma.typeMachine.create({
data: {
...typeData,
customFields: customFields ? {
create: customFields.map(field => ({
name: field.name,
type: field.type,
required: field.required || false,
defaultValue: field.defaultValue,
options: field.options
}))
} : undefined,
componentRequirements: componentRequirements && componentRequirements.length > 0 ? {
create: componentRequirements.map(requirement => ({
label: requirement.label,
minCount: requirement.minCount ?? 1,
maxCount: requirement.maxCount ?? null,
required: requirement.required ?? true,
allowNewModels: requirement.allowNewModels ?? true,
typeComposant: {
connect: { id: requirement.typeComposantId },
},
}))
} : undefined,
pieceRequirements: pieceRequirements && pieceRequirements.length > 0 ? {
create: pieceRequirements.map(requirement => ({
label: requirement.label,
minCount: requirement.minCount ?? 0,
maxCount: requirement.maxCount ?? null,
required: requirement.required ?? false,
allowNewModels: requirement.allowNewModels ?? true,
typePiece: {
connect: { id: requirement.typePieceId },
},
}))
} : undefined,
customFields: customFields
? {
create: customFields.map((field) => ({
name: field.name,
type: field.type,
required: field.required || false,
defaultValue: field.defaultValue,
options: field.options,
})),
}
: undefined,
componentRequirements:
componentRequirements && componentRequirements.length > 0
? {
create: componentRequirements.map((requirement) => ({
label: requirement.label,
minCount: requirement.minCount ?? 1,
maxCount: requirement.maxCount ?? null,
required: requirement.required ?? true,
allowNewModels: requirement.allowNewModels ?? true,
typeComposant: {
connect: { id: requirement.typeComposantId },
},
})),
}
: undefined,
pieceRequirements:
pieceRequirements && pieceRequirements.length > 0
? {
create: pieceRequirements.map((requirement) => ({
label: requirement.label,
minCount: requirement.minCount ?? 0,
maxCount: requirement.maxCount ?? null,
required: requirement.required ?? false,
allowNewModels: requirement.allowNewModels ?? true,
typePiece: {
connect: { id: requirement.typePieceId },
},
})),
}
: undefined,
},
include: {
customFields: true,
@@ -113,27 +154,35 @@ export class TypesService {
});
}
async updateTypeMachine(id: string, updateTypeMachineDto: UpdateTypeMachineDto) {
const { customFields, componentRequirements, pieceRequirements, ...typeData } = updateTypeMachineDto;
async updateTypeMachine(
id: string,
updateTypeMachineDto: UpdateTypeMachineDto,
) {
const {
customFields,
componentRequirements,
pieceRequirements,
...typeData
} = updateTypeMachineDto;
// Si des champs personnalisés sont fournis, on les met à jour
if (customFields !== undefined) {
// Supprimer tous les champs personnalisés existants
await this.prisma.customField.deleteMany({
where: { typeMachineId: id }
where: { typeMachineId: id },
});
// Créer les nouveaux champs personnalisés
if (customFields.length > 0) {
await this.prisma.customField.createMany({
data: customFields.map(field => ({
data: customFields.map((field) => ({
name: field.name,
type: field.type,
required: field.required || false,
defaultValue: field.defaultValue,
options: field.options,
typeMachineId: id
}))
typeMachineId: id,
})),
});
}
}
@@ -145,7 +194,7 @@ export class TypesService {
if (componentRequirements.length > 0) {
await this.prisma.typeMachineComponentRequirement.createMany({
data: componentRequirements.map(requirement => ({
data: componentRequirements.map((requirement) => ({
label: requirement.label ?? null,
minCount: requirement.minCount ?? 1,
maxCount: requirement.maxCount ?? null,
@@ -166,7 +215,7 @@ export class TypesService {
if (pieceRequirements.length > 0) {
await this.prisma.typeMachinePieceRequirement.createMany({
data: pieceRequirements.map(requirement => ({
data: pieceRequirements.map((requirement) => ({
label: requirement.label ?? null,
minCount: requirement.minCount ?? 0,
maxCount: requirement.maxCount ?? null,
@@ -179,7 +228,7 @@ export class TypesService {
});
}
}
return this.prisma.typeMachine.update({
where: { id },
data: typeData,
@@ -207,10 +256,10 @@ export class TypesService {
});
if (machinesWithType.length > 0) {
const machineNames = machinesWithType.map(m => m.name).join(', ');
const machineNames = machinesWithType.map((m) => m.name).join(', ');
throw new Error(
`Impossible de supprimer ce type de machine car il est utilisé par ${machinesWithType.length} machine(s): ${machineNames}. ` +
`Veuillez d'abord supprimer ou modifier ces machines.`
`Veuillez d'abord supprimer ou modifier ces machines.`,
);
}
@@ -224,22 +273,30 @@ export class TypesService {
});
}
// TypeComposant methods
// TypeComposant methods (backed by ModelType with COMPONENT category)
async createTypeComposant(createTypeComposantDto: CreateTypeComposantDto) {
const { customFields, ...typeData } = createTypeComposantDto;
return this.prisma.typeComposant.create({
const { customFields, description, name } = createTypeComposantDto;
const code = await this.generateUniqueModelTypeCode(name);
return this.prisma.modelType.create({
data: {
...typeData,
customFields: customFields ? {
create: customFields.map(field => ({
name: field.name,
type: field.type,
required: field.required || false,
defaultValue: field.defaultValue,
options: field.options
}))
} : undefined
name,
code,
category: ModelCategory.COMPONENT,
description: description ?? null,
notes: description ?? null,
customFields: customFields
? {
create: customFields.map((field) => ({
name: field.name,
type: field.type,
required: field.required || false,
defaultValue: field.defaultValue,
options: field.options,
})),
}
: undefined,
},
include: {
customFields: true,
@@ -248,7 +305,8 @@ export class TypesService {
}
async findAllTypeComposants() {
return this.prisma.typeComposant.findMany({
return this.prisma.modelType.findMany({
where: { category: ModelCategory.COMPONENT },
include: {
composants: true,
customFields: true,
@@ -258,8 +316,11 @@ export class TypesService {
}
async findOneTypeComposant(id: string) {
return this.prisma.typeComposant.findUnique({
where: { id },
return this.prisma.modelType.findFirst({
where: {
id,
category: ModelCategory.COMPONENT,
},
include: {
composants: true,
customFields: true,
@@ -268,34 +329,43 @@ export class TypesService {
});
}
async updateTypeComposant(id: string, updateTypeComposantDto: UpdateTypeComposantDto) {
const { customFields, ...typeData } = updateTypeComposantDto;
// Si des champs personnalisés sont fournis, on les met à jour
async updateTypeComposant(
id: string,
updateTypeComposantDto: UpdateTypeComposantDto,
) {
const { customFields, description, name } = updateTypeComposantDto;
if (customFields !== undefined) {
// Supprimer tous les champs personnalisés existants
await this.prisma.customField.deleteMany({
where: { typeComposantId: id }
where: { typeComposantId: id },
});
// Créer les nouveaux champs personnalisés
if (customFields.length > 0) {
await this.prisma.customField.createMany({
data: customFields.map(field => ({
data: customFields.map((field) => ({
name: field.name,
type: field.type,
required: field.required || false,
defaultValue: field.defaultValue,
options: field.options,
typeComposantId: id
}))
typeComposantId: id,
})),
});
}
}
return this.prisma.typeComposant.update({
const data: Record<string, unknown> = {};
if (name !== undefined) {
data.name = name;
}
if (description !== undefined) {
data.description = description;
data.notes = description;
}
return this.prisma.modelType.update({
where: { id },
data: typeData,
data,
include: {
customFields: true,
},
@@ -303,91 +373,145 @@ export class TypesService {
}
async removeTypeComposant(id: string) {
return this.prisma.typeComposant.delete({
return this.prisma.modelType.delete({
where: { id },
});
}
// TypePiece methods
// TypePiece methods backed by ModelType
private mapModelTypePiece(modelType: any) {
if (!modelType) {
return null;
}
const {
pieceCustomFields,
pieceModels,
pieceRequirements,
pieces,
...rest
} = modelType;
return {
...rest,
customFields: pieceCustomFields ?? [],
models: pieceModels ?? [],
pieceRequirements: pieceRequirements ?? [],
pieces: pieces ?? [],
};
}
async createTypePiece(createTypePieceDto: CreateTypePieceDto) {
const { customFields, ...typeData } = createTypePieceDto;
return this.prisma.typePiece.create({
const { customFields, description, name } = createTypePieceDto;
const code = await this.generateUniqueModelTypeCode(name);
const created = await this.prisma.modelType.create({
data: {
...typeData,
customFields: customFields ? {
create: customFields.map(field => ({
name: field.name,
type: field.type,
required: field.required || false,
defaultValue: field.defaultValue,
options: field.options
}))
} : undefined
name,
code,
category: ModelCategory.PIECE,
description: description ?? null,
notes: description ?? null,
pieceCustomFields: customFields
? {
create: customFields.map((field) => ({
name: field.name,
type: field.type,
required: field.required || false,
defaultValue: field.defaultValue,
options: field.options,
})),
}
: undefined,
},
include: {
customFields: true,
pieceCustomFields: true,
pieceModels: true,
pieceRequirements: true,
pieces: true,
},
});
return this.mapModelTypePiece(created);
}
async findAllTypePieces() {
return this.prisma.typePiece.findMany({
const items = await this.prisma.modelType.findMany({
where: { category: ModelCategory.PIECE },
include: {
pieceCustomFields: true,
pieceModels: true,
pieceRequirements: true,
pieces: true,
customFields: true,
models: true,
},
orderBy: { name: 'asc' },
});
return items.map((item) => this.mapModelTypePiece(item));
}
async findOneTypePiece(id: string) {
return this.prisma.typePiece.findUnique({
where: { id },
const item = await this.prisma.modelType.findFirst({
where: { id, category: ModelCategory.PIECE },
include: {
pieceCustomFields: true,
pieceModels: true,
pieceRequirements: true,
pieces: true,
customFields: true,
models: true,
},
});
return this.mapModelTypePiece(item);
}
async updateTypePiece(id: string, updateTypePieceDto: UpdateTypePieceDto) {
const { customFields, ...typeData } = updateTypePieceDto;
// Si des champs personnalisés sont fournis, on les met à jour
const { customFields, description, name } = updateTypePieceDto;
if (customFields !== undefined) {
// Supprimer tous les champs personnalisés existants
await this.prisma.customField.deleteMany({
where: { typePieceId: id }
where: { typePieceId: id },
});
// Créer les nouveaux champs personnalisés
if (customFields.length > 0) {
await this.prisma.customField.createMany({
data: customFields.map(field => ({
data: customFields.map((field) => ({
name: field.name,
type: field.type,
required: field.required || false,
defaultValue: field.defaultValue,
options: field.options,
typePieceId: id
}))
typePieceId: id,
})),
});
}
}
return this.prisma.typePiece.update({
const updatePayload: Record<string, unknown> = {};
if (name !== undefined) {
updatePayload.name = name;
}
if (description !== undefined) {
updatePayload.description = description;
updatePayload.notes = description;
}
const updated = await this.prisma.modelType.update({
where: { id },
data: typeData,
data: updatePayload,
include: {
customFields: true,
pieceCustomFields: true,
pieceModels: true,
pieceRequirements: true,
pieces: true,
},
});
return this.mapModelTypePiece(updated);
}
async removeTypePiece(id: string) {
return this.prisma.typePiece.delete({
return this.prisma.modelType.delete({
where: { id },
});
}
@@ -430,7 +554,10 @@ export class TypesService {
});
}
async updateComposantModel(id: string, updateComposantModelDto: UpdateComposantModelDto) {
async updateComposantModel(
id: string,
updateComposantModelDto: UpdateComposantModelDto,
) {
const { typeComposantId, ...data } = updateComposantModelDto;
return this.prisma.composantModel.update({

View File

@@ -156,9 +156,12 @@ class InMemoryPrismaService {
private sites: SiteRecord[] = [];
private typeComposants: TypeComposantRecord[] = [];
private typePieces: TypePieceRecord[] = [];
private modelTypeCodeCounter = 0;
private typeMachines: TypeMachineRecord[] = [];
private typeMachineComponentRequirements: TypeMachineComponentRequirementRecord[] = [];
private typeMachinePieceRequirements: TypeMachinePieceRequirementRecord[] = [];
private typeMachineComponentRequirements: TypeMachineComponentRequirementRecord[] =
[];
private typeMachinePieceRequirements: TypeMachinePieceRequirementRecord[] =
[];
private machines: MachineRecord[] = [];
private composants: ComposantRecord[] = [];
private pieces: PieceRecord[] = [];
@@ -179,6 +182,7 @@ class InMemoryPrismaService {
this.sites = [];
this.typeComposants = [];
this.typePieces = [];
this.modelTypeCodeCounter = 0;
this.typeMachines = [];
this.typeMachineComponentRequirements = [];
this.typeMachinePieceRequirements = [];
@@ -268,7 +272,9 @@ class InMemoryPrismaService {
return include?.customFields
? {
...record,
customFields: this.customFields.filter((field) => field.typeComposantId === record.id),
customFields: this.customFields.filter(
(field) => field.typeComposantId === record.id,
),
}
: { ...record };
},
@@ -280,7 +286,9 @@ class InMemoryPrismaService {
return this.typeComposants.find((item) => item.id === where.id) ?? null;
}
if (where.name) {
return this.typeComposants.find((item) => item.name === where.name) ?? null;
return (
this.typeComposants.find((item) => item.name === where.name) ?? null
);
}
return null;
},
@@ -325,7 +333,9 @@ class InMemoryPrismaService {
return include?.customFields
? {
...record,
customFields: this.customFields.filter((field) => field.typePieceId === record.id),
customFields: this.customFields.filter(
(field) => field.typePieceId === record.id,
),
}
: { ...record };
},
@@ -383,20 +393,22 @@ class InMemoryPrismaService {
let componentRequirements: TypeMachineComponentRequirementRecord[] = [];
if (data.componentRequirements?.create) {
componentRequirements = data.componentRequirements.create.map((requirement) => {
const req: TypeMachineComponentRequirementRecord = {
id: generateId('tmc_req'),
typeMachineId: record.id,
typeComposantId: requirement.typeComposant.connect.id,
label: requirement.label ?? null,
minCount: requirement.minCount ?? 1,
maxCount: requirement.maxCount ?? null,
required: requirement.required ?? true,
allowNewModels: requirement.allowNewModels ?? true,
};
this.typeMachineComponentRequirements.push(req);
return req;
});
componentRequirements = data.componentRequirements.create.map(
(requirement) => {
const req: TypeMachineComponentRequirementRecord = {
id: generateId('tmc_req'),
typeMachineId: record.id,
typeComposantId: requirement.typeComposant.connect.id,
label: requirement.label ?? null,
minCount: requirement.minCount ?? 1,
maxCount: requirement.maxCount ?? null,
required: requirement.required ?? true,
allowNewModels: requirement.allowNewModels ?? true,
};
this.typeMachineComponentRequirements.push(req);
return req;
},
);
}
let pieceRequirements: TypeMachinePieceRequirementRecord[] = [];
@@ -417,28 +429,49 @@ class InMemoryPrismaService {
});
}
return this.buildTypeMachine(record, include ?? {}, componentRequirements, pieceRequirements);
return this.buildTypeMachine(
record,
include ?? {},
componentRequirements,
pieceRequirements,
);
},
findUnique: async ({ where, include }: any) => {
const typeMachine = this.typeMachines.find((item) => item.id === where.id);
const typeMachine = this.typeMachines.find(
(item) => item.id === where.id,
);
if (!typeMachine) {
return null;
}
const componentRequirements = this.typeMachineComponentRequirements.filter(
(item) => item.typeMachineId === typeMachine.id,
);
const componentRequirements =
this.typeMachineComponentRequirements.filter(
(item) => item.typeMachineId === typeMachine.id,
);
const pieceRequirements = this.typeMachinePieceRequirements.filter(
(item) => item.typeMachineId === typeMachine.id,
);
return this.buildTypeMachine(typeMachine, include ?? {}, componentRequirements, pieceRequirements);
return this.buildTypeMachine(
typeMachine,
include ?? {},
componentRequirements,
pieceRequirements,
);
},
findMany: async ({ include }: any = {}) => {
return this.typeMachines.map((item) => {
const componentRequirements = this.typeMachineComponentRequirements.filter(
const componentRequirements =
this.typeMachineComponentRequirements.filter(
(req) => req.typeMachineId === item.id,
);
const pieceRequirements = this.typeMachinePieceRequirements.filter(
(req) => req.typeMachineId === item.id,
);
const pieceRequirements = this.typeMachinePieceRequirements.filter((req) => req.typeMachineId === item.id);
return this.buildTypeMachine(item, include ?? {}, componentRequirements, pieceRequirements);
return this.buildTypeMachine(
item,
include ?? {},
componentRequirements,
pieceRequirements,
);
});
},
};
@@ -471,9 +504,13 @@ class InMemoryPrismaService {
findMany: async ({ include, where }: any = {}) => {
let machines = this.machines;
if (where?.siteId) {
machines = machines.filter((machine) => machine.siteId === where.siteId);
machines = machines.filter(
(machine) => machine.siteId === where.siteId,
);
}
return machines.map((machine) => this.buildMachine(machine, include ?? {}));
return machines.map((machine) =>
this.buildMachine(machine, include ?? {}),
);
},
delete: async ({ where }: any) => {
const index = this.machines.findIndex((item) => item.id === where.id);
@@ -498,7 +535,8 @@ class InMemoryPrismaService {
parentComposantId: data.parentComposantId ?? null,
typeComposantId: data.typeComposantId ?? null,
composantModelId: data.composantModelId ?? null,
typeMachineComponentRequirementId: data.typeMachineComponentRequirementId ?? null,
typeMachineComponentRequirementId:
data.typeMachineComponentRequirementId ?? null,
constructeurId: data.constructeurId ?? null,
createdAt: now,
updatedAt: now,
@@ -509,10 +547,14 @@ class InMemoryPrismaService {
findMany: async ({ where }: any) => {
let composants = this.composants;
if (where?.machineId !== undefined) {
composants = composants.filter((item) => item.machineId === where.machineId);
composants = composants.filter(
(item) => item.machineId === where.machineId,
);
}
if (where?.parentComposantId !== undefined) {
composants = composants.filter((item) => item.parentComposantId === where.parentComposantId);
composants = composants.filter(
(item) => item.parentComposantId === where.parentComposantId,
);
}
return composants.map((item) => ({ ...item }));
},
@@ -531,7 +573,8 @@ class InMemoryPrismaService {
composantId: data.composantId ?? null,
typePieceId: data.typePieceId ?? null,
pieceModelId: data.pieceModelId ?? null,
typeMachinePieceRequirementId: data.typeMachinePieceRequirementId ?? null,
typeMachinePieceRequirementId:
data.typeMachinePieceRequirementId ?? null,
constructeurId: data.constructeurId ?? null,
createdAt: now,
updatedAt: now,
@@ -545,7 +588,9 @@ class InMemoryPrismaService {
pieces = pieces.filter((item) => item.machineId === where.machineId);
}
if (where?.composantId !== undefined) {
pieces = pieces.filter((item) => item.composantId === where.composantId);
pieces = pieces.filter(
(item) => item.composantId === where.composantId,
);
}
return pieces.map((item) => ({ ...item }));
},
@@ -574,14 +619,18 @@ class InMemoryPrismaService {
return this.customFields
.filter((field) => {
if (!where) return true;
return Object.entries(where).every(([key, value]) => (field as any)[key] === value);
return Object.entries(where).every(
([key, value]) => (field as any)[key] === value,
);
})
.map((field) => ({ ...field }));
},
deleteMany: async ({ where }: any) => {
const before = this.customFields.length;
this.customFields = this.customFields.filter((field) => {
return !Object.entries(where).every(([key, value]) => (field as any)[key] === value);
return !Object.entries(where).every(
([key, value]) => (field as any)[key] === value,
);
});
return { count: before - this.customFields.length };
},
@@ -606,12 +655,18 @@ class InMemoryPrismaService {
findMany: async ({ where, include }: any) => {
const records = this.customFieldValues.filter((value) => {
if (!where) return true;
return Object.entries(where).every(([key, v]) => (value as any)[key] === v);
return Object.entries(where).every(
([key, v]) => (value as any)[key] === v,
);
});
return records.map((record) => this.buildCustomFieldValue(record, include ?? {}));
return records.map((record) =>
this.buildCustomFieldValue(record, include ?? {}),
);
},
findUnique: async ({ where, include }: any) => {
const record = this.customFieldValues.find((value) => value.id === where.id);
const record = this.customFieldValues.find(
(value) => value.id === where.id,
);
if (!record) {
return null;
}
@@ -625,7 +680,9 @@ class InMemoryPrismaService {
);
},
update: async ({ where, data, include }: any) => {
const record = this.customFieldValues.find((value) => value.id === where.id);
const record = this.customFieldValues.find(
(value) => value.id === where.id,
);
if (!record) {
throw new Error('Custom field value not found');
}
@@ -636,7 +693,9 @@ class InMemoryPrismaService {
return this.buildCustomFieldValue(record, include ?? {});
},
delete: async ({ where }: any) => {
const index = this.customFieldValues.findIndex((value) => value.id === where.id);
const index = this.customFieldValues.findIndex(
(value) => value.id === where.id,
);
if (index === -1) {
throw new Error('Custom field value not found');
}
@@ -649,24 +708,32 @@ class InMemoryPrismaService {
count: async ({ where }: any) => {
return this.profiles.filter((profile) => {
if (!where) return true;
return Object.entries(where).every(([key, value]) => (profile as any)[key] === value);
return Object.entries(where).every(
([key, value]) => (profile as any)[key] === value,
);
}).length;
},
findMany: async ({ where, orderBy, select }: any) => {
let profiles = this.profiles;
if (where) {
profiles = profiles.filter((profile) =>
Object.entries(where).every(([key, value]) => (profile as any)[key] === value),
Object.entries(where).every(
([key, value]) => (profile as any)[key] === value,
),
);
}
if (orderBy?.createdAt === 'asc') {
profiles = [...profiles].sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
profiles = [...profiles].sort(
(a, b) => a.createdAt.getTime() - b.createdAt.getTime(),
);
}
return profiles.map((profile) => this.applySelect(profile, select));
},
findFirst: async ({ where, select }: any) => {
const profile = this.profiles.find((item) =>
Object.entries(where).every(([key, value]) => (item as any)[key] === value),
Object.entries(where).every(
([key, value]) => (item as any)[key] === value,
),
);
return profile ? this.applySelect(profile, select) : null;
},
@@ -713,8 +780,12 @@ class InMemoryPrismaService {
private buildSite(site: SiteRecord, include: any) {
const base: any = { ...site };
if (include?.machines) {
const machines = this.machines.filter((machine) => machine.siteId === site.id);
base.machines = machines.map((machine) => this.buildMachine(machine, include.machines.include ?? {}));
const machines = this.machines.filter(
(machine) => machine.siteId === site.id,
);
base.machines = machines.map((machine) =>
this.buildMachine(machine, include.machines.include ?? {}),
);
}
if (include?.documents) {
base.documents = [];
@@ -730,13 +801,17 @@ class InMemoryPrismaService {
) {
const base: any = { ...record };
if (include?.customFields) {
base.customFields = this.customFields.filter((field) => field.typeMachineId === record.id);
base.customFields = this.customFields.filter(
(field) => field.typeMachineId === record.id,
);
}
if (include?.componentRequirements) {
base.componentRequirements = componentRequirements.map((requirement) => ({
...requirement,
typeComposant: include.componentRequirements.include?.typeComposant
? this.typeComposants.find((item) => item.id === requirement.typeComposantId) ?? null
? (this.typeComposants.find(
(item) => item.id === requirement.typeComposantId,
) ?? null)
: undefined,
}));
}
@@ -744,14 +819,18 @@ class InMemoryPrismaService {
base.pieceRequirements = pieceRequirements.map((requirement) => ({
...requirement,
typePiece: include.pieceRequirements.include?.typePiece
? this.typePieces.find((item) => item.id === requirement.typePieceId) ?? null
? (this.typePieces.find(
(item) => item.id === requirement.typePieceId,
) ?? null)
: undefined,
}));
}
if (include?.machines) {
base.machines = this.machines
.filter((machine) => machine.typeMachineId === record.id)
.map((machine) => this.buildMachine(machine, include.machines.include ?? {}));
.map((machine) =>
this.buildMachine(machine, include.machines.include ?? {}),
);
}
return base;
}
@@ -768,9 +847,10 @@ class InMemoryPrismaService {
? this.typeMachines.find((item) => item.id === machine.typeMachineId)
: null;
if (typeMachine) {
const componentRequirements = this.typeMachineComponentRequirements.filter(
(req) => req.typeMachineId === typeMachine.id,
);
const componentRequirements =
this.typeMachineComponentRequirements.filter(
(req) => req.typeMachineId === typeMachine.id,
);
const pieceRequirements = this.typeMachinePieceRequirements.filter(
(req) => req.typeMachineId === typeMachine.id,
);
@@ -790,20 +870,35 @@ class InMemoryPrismaService {
}
if (include?.composants) {
const composants = this.composants.filter((component) => component.machineId === machine.id);
const composants = this.composants.filter(
(component) => component.machineId === machine.id,
);
base.composants = composants
.filter((component) => component.parentComposantId === null)
.map((component) => this.buildComponent(component, include.composants.include ?? {}));
.map((component) =>
this.buildComponent(component, include.composants.include ?? {}),
);
}
if (include?.pieces) {
const machinePieces = this.pieces.filter((piece) => piece.machineId === machine.id && piece.composantId === null);
base.pieces = machinePieces.map((piece) => this.buildPiece(piece, include.pieces.include ?? {}));
const machinePieces = this.pieces.filter(
(piece) => piece.machineId === machine.id && piece.composantId === null,
);
base.pieces = machinePieces.map((piece) =>
this.buildPiece(piece, include.pieces.include ?? {}),
);
}
if (include?.customFieldValues) {
const values = this.customFieldValues.filter((value) => value.machineId === machine.id);
base.customFieldValues = values.map((value) => this.buildCustomFieldValue(value, include.customFieldValues.include ?? {}));
const values = this.customFieldValues.filter(
(value) => value.machineId === machine.id,
);
base.customFieldValues = values.map((value) =>
this.buildCustomFieldValue(
value,
include.customFieldValues.include ?? {},
),
);
}
if (include?.documents) {
@@ -818,7 +913,9 @@ class InMemoryPrismaService {
if (include?.typeComposant) {
base.typeComposant = component.typeComposantId
? this.typeComposants.find((item) => item.id === component.typeComposantId) ?? null
? (this.typeComposants.find(
(item) => item.id === component.typeComposantId,
) ?? null)
: null;
}
@@ -828,26 +925,42 @@ class InMemoryPrismaService {
if (include?.typeMachineComponentRequirement) {
const requirement = component.typeMachineComponentRequirementId
? this.typeMachineComponentRequirements.find((item) => item.id === component.typeMachineComponentRequirementId) ?? null
? (this.typeMachineComponentRequirements.find(
(item) => item.id === component.typeMachineComponentRequirementId,
) ?? null)
: null;
base.typeMachineComponentRequirement = requirement
? {
...requirement,
typeComposant: include.typeMachineComponentRequirement.include?.typeComposant
? this.typeComposants.find((item) => item.id === requirement.typeComposantId) ?? null
typeComposant: include.typeMachineComponentRequirement.include
?.typeComposant
? (this.typeComposants.find(
(item) => item.id === requirement.typeComposantId,
) ?? null)
: undefined,
}
: null;
}
if (include?.sousComposants) {
const children = this.composants.filter((item) => item.parentComposantId === component.id);
base.sousComposants = children.map((child) => this.buildComponent(child, include.sousComposants.include ?? {}));
const children = this.composants.filter(
(item) => item.parentComposantId === component.id,
);
base.sousComposants = children.map((child) =>
this.buildComponent(child, include.sousComposants.include ?? {}),
);
}
if (include?.customFieldValues) {
const values = this.customFieldValues.filter((value) => value.composantId === component.id);
base.customFieldValues = values.map((value) => this.buildCustomFieldValue(value, include.customFieldValues.include ?? {}));
const values = this.customFieldValues.filter(
(value) => value.composantId === component.id,
);
base.customFieldValues = values.map((value) =>
this.buildCustomFieldValue(
value,
include.customFieldValues.include ?? {},
),
);
}
if (include?.constructeur) {
@@ -855,8 +968,12 @@ class InMemoryPrismaService {
}
if (include?.pieces) {
const relatedPieces = this.pieces.filter((piece) => piece.composantId === component.id);
base.pieces = relatedPieces.map((piece) => this.buildPiece(piece, include.pieces.include ?? {}));
const relatedPieces = this.pieces.filter(
(piece) => piece.composantId === component.id,
);
base.pieces = relatedPieces.map((piece) =>
this.buildPiece(piece, include.pieces.include ?? {}),
);
}
return base;
@@ -866,8 +983,15 @@ class InMemoryPrismaService {
const base: any = { ...piece };
if (include?.customFieldValues) {
const values = this.customFieldValues.filter((value) => value.pieceId === piece.id);
base.customFieldValues = values.map((value) => this.buildCustomFieldValue(value, include.customFieldValues.include ?? {}));
const values = this.customFieldValues.filter(
(value) => value.pieceId === piece.id,
);
base.customFieldValues = values.map((value) =>
this.buildCustomFieldValue(
value,
include.customFieldValues.include ?? {},
),
);
}
if (include?.constructeur) {
@@ -880,13 +1004,17 @@ class InMemoryPrismaService {
if (include?.typeMachinePieceRequirement) {
const requirement = piece.typeMachinePieceRequirementId
? this.typeMachinePieceRequirements.find((item) => item.id === piece.typeMachinePieceRequirementId) ?? null
? (this.typeMachinePieceRequirements.find(
(item) => item.id === piece.typeMachinePieceRequirementId,
) ?? null)
: null;
base.typeMachinePieceRequirement = requirement
? {
...requirement,
typePiece: include.typeMachinePieceRequirement.include?.typePiece
? this.typePieces.find((item) => item.id === requirement.typePieceId) ?? null
? (this.typePieces.find(
(item) => item.id === requirement.typePieceId,
) ?? null)
: undefined,
}
: null;
@@ -894,7 +1022,8 @@ class InMemoryPrismaService {
if (include?.typePiece) {
base.typePiece = piece.typePieceId
? this.typePieces.find((item) => item.id === piece.typePieceId) ?? null
? (this.typePieces.find((item) => item.id === piece.typePieceId) ??
null)
: null;
}
@@ -904,7 +1033,9 @@ class InMemoryPrismaService {
private buildCustomFieldValue(record: CustomFieldValueRecord, include: any) {
const base: any = { ...record };
if (include?.customField) {
base.customField = this.customFields.find((field) => field.id === record.customFieldId) ?? null;
base.customField =
this.customFields.find((field) => field.id === record.customFieldId) ??
null;
}
return base;
}
@@ -918,7 +1049,6 @@ describe('Inventory flow (e2e)', () => {
beforeAll(async () => {
prismaStub = new InMemoryPrismaService();
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
})
@@ -945,14 +1075,16 @@ describe('Inventory flow (e2e)', () => {
});
it('should create a type, create a machine with new component/piece selections, and edit technical fields', async () => {
const siteResponse = await request(app.getHttpServer()).post('/sites').send({
name: 'Site Principal',
contactName: 'Responsable Maintenance',
contactPhone: '+33 1 23 45 67 89',
contactAddress: '1 rue de la Paix',
contactPostalCode: '75000',
contactCity: 'Paris',
});
const siteResponse = await request(app.getHttpServer())
.post('/sites')
.send({
name: 'Site Principal',
contactName: 'Responsable Maintenance',
contactPhone: '+33 1 23 45 67 89',
contactAddress: '1 rue de la Paix',
contactPostalCode: '75000',
contactCity: 'Paris',
});
expect(siteResponse.status).toBe(201);
const siteId = siteResponse.body.id;
@@ -1069,7 +1201,9 @@ describe('Inventory flow (e2e)', () => {
expect(machineResponse.status).toBe(201);
const machineId = machineResponse.body.id;
const machineDetailsResponse = await request(app.getHttpServer()).get(`/machines/${machineId}`);
const machineDetailsResponse = await request(app.getHttpServer()).get(
`/machines/${machineId}`,
);
expect(machineDetailsResponse.status).toBe(200);
const machine = machineDetailsResponse.body;
@@ -1092,7 +1226,9 @@ describe('Inventory flow (e2e)', () => {
expect(updateResponse.status).toBe(200);
expect(updateResponse.body.value).toBe('8 kW');
const refreshedMachineResponse = await request(app.getHttpServer()).get(`/machines/${machine.id}`);
const refreshedMachineResponse = await request(app.getHttpServer()).get(
`/machines/${machine.id}`,
);
expect(refreshedMachineResponse.status).toBe(200);
const refreshedComponent = refreshedMachineResponse.body.composants[0];
expect(refreshedComponent.customFieldValues[0].value).toBe('8 kW');
@@ -1110,7 +1246,9 @@ describe('Inventory flow (e2e)', () => {
} as any);
const created = { id: 'component-1' };
const createSpy = jest.spyOn(prisma.composant, 'create').mockResolvedValue(created as any);
const createSpy = jest
.spyOn(prisma.composant, 'create')
.mockResolvedValue(created as any);
const response = await request(app.getHttpServer())
.post('/composants')
@@ -1157,14 +1295,14 @@ describe('Inventory flow (e2e)', () => {
jest.spyOn(prisma.machine, 'findUnique').mockResolvedValue({
id: 'machine-1',
typeMachine: {
pieceRequirements: [
{ id: 'req-1', typePieceId: 'type-piece-1' },
],
pieceRequirements: [{ id: 'req-1', typePieceId: 'type-piece-1' }],
},
} as any);
const created = { id: 'piece-1' };
const createSpy = jest.spyOn(prisma.piece, 'create').mockResolvedValue(created as any);
const createSpy = jest
.spyOn(prisma.piece, 'create')
.mockResolvedValue(created as any);
const response = await request(app.getHttpServer())
.post('/pieces')
@@ -1184,9 +1322,7 @@ describe('Inventory flow (e2e)', () => {
jest.spyOn(prisma.machine, 'findUnique').mockResolvedValue({
id: 'machine-1',
typeMachine: {
pieceRequirements: [
{ id: 'req-1', typePieceId: 'type-piece-1' },
],
pieceRequirements: [{ id: 'req-1', typePieceId: 'type-piece-1' }],
},
} as any);
@@ -1203,7 +1339,6 @@ describe('Inventory flow (e2e)', () => {
.expect(400);
expect(createSpy).not.toHaveBeenCalled();
});
});
});