From 83251b532c5a76b36524c54cdd7c18e42f10f4fd Mon Sep 17 00:00:00 2001 From: Matthieu Date: Wed, 17 Sep 2025 15:09:54 +0200 Subject: [PATCH] feat(constructeurs): introduce constructors management --- .../migration.sql | 78 +++++++++++++++++++ prisma/schema.prisma | 27 ++++++- src/app.module.ts | 2 + src/composants/composants.service.ts | 22 ++++++ src/constructeurs/constructeurs.controller.ts | 33 ++++++++ src/constructeurs/constructeurs.module.ts | 10 +++ src/constructeurs/constructeurs.service.ts | 64 +++++++++++++++ src/machines/machines.service.ts | 65 +++++++++++++++- src/pieces/pieces.service.ts | 6 ++ src/shared/dto/composant.dto.ts | 4 +- src/shared/dto/constructeur.dto.ts | 28 +++++++ src/shared/dto/machine.dto.ts | 6 +- src/shared/dto/piece.dto.ts | 4 +- 13 files changed, 335 insertions(+), 14 deletions(-) create mode 100644 prisma/migrations/20250920095000_add_constructeurs/migration.sql create mode 100644 src/constructeurs/constructeurs.controller.ts create mode 100644 src/constructeurs/constructeurs.module.ts create mode 100644 src/constructeurs/constructeurs.service.ts create mode 100644 src/shared/dto/constructeur.dto.ts diff --git a/prisma/migrations/20250920095000_add_constructeurs/migration.sql b/prisma/migrations/20250920095000_add_constructeurs/migration.sql new file mode 100644 index 0000000..637000a --- /dev/null +++ b/prisma/migrations/20250920095000_add_constructeurs/migration.sql @@ -0,0 +1,78 @@ +-- Create constructeurs table +CREATE TABLE "constructeurs" ( + "id" TEXT PRIMARY KEY, + "name" TEXT NOT NULL, + "email" TEXT, + "phone" TEXT, + "createdAt" TIMESTAMP NOT NULL DEFAULT NOW(), + "updatedAt" TIMESTAMP NOT NULL DEFAULT NOW() +); + +CREATE UNIQUE INDEX "constructeurs_name_unique_idx" ON "constructeurs"(LOWER("name")); + +-- Add foreign key columns to machines, composants, pieces +ALTER TABLE "machines" ADD COLUMN "constructeurId" TEXT; +ALTER TABLE "composants" ADD COLUMN "constructeurId" TEXT; +ALTER TABLE "pieces" ADD COLUMN "constructeurId" TEXT; + +-- Populate constructeurs from existing data +WITH existing_names AS ( + SELECT DISTINCT TRIM(constructeur) AS name FROM "machines" WHERE constructeur IS NOT NULL AND TRIM(constructeur) <> '' + UNION + SELECT DISTINCT TRIM(constructeur) FROM "composants" WHERE constructeur IS NOT NULL AND TRIM(constructeur) <> '' + UNION + SELECT DISTINCT TRIM(constructeur) FROM "pieces" WHERE constructeur IS NOT NULL AND TRIM(constructeur) <> '' +), +prepared AS ( + SELECT DISTINCT ON (LOWER(name)) name + FROM existing_names + WHERE name IS NOT NULL AND name <> '' + ORDER BY LOWER(name), name +) +INSERT INTO "constructeurs" ("id", "name") +SELECT substr(md5(random()::text || clock_timestamp()::text), 1, 25), name +FROM prepared +ON CONFLICT (LOWER("name")) DO NOTHING; + +-- Link existing records to constructeurs +UPDATE "machines" m +SET "constructeurId" = c.id +FROM "constructeurs" c +WHERE m.constructeur IS NOT NULL + AND TRIM(m.constructeur) <> '' + AND LOWER(c.name) = LOWER(TRIM(m.constructeur)); + +UPDATE "composants" co +SET "constructeurId" = c.id +FROM "constructeurs" c +WHERE co.constructeur IS NOT NULL + AND TRIM(co.constructeur) <> '' + AND LOWER(c.name) = LOWER(TRIM(co.constructeur)); + +UPDATE "pieces" p +SET "constructeurId" = c.id +FROM "constructeurs" c +WHERE p.constructeur IS NOT NULL + AND TRIM(p.constructeur) <> '' + AND LOWER(c.name) = LOWER(TRIM(p.constructeur)); + +-- Drop legacy constructeur columns +ALTER TABLE "machines" DROP COLUMN "constructeur"; +ALTER TABLE "composants" DROP COLUMN "constructeur"; +ALTER TABLE "pieces" DROP COLUMN "constructeur"; + +-- Add foreign key constraints +ALTER TABLE "machines" + ADD CONSTRAINT "machines_constructeurId_fkey" + FOREIGN KEY ("constructeurId") REFERENCES "constructeurs"("id") + ON DELETE SET NULL ON UPDATE CASCADE; + +ALTER TABLE "composants" + ADD CONSTRAINT "composants_constructeurId_fkey" + FOREIGN KEY ("constructeurId") REFERENCES "constructeurs"("id") + ON DELETE SET NULL ON UPDATE CASCADE; + +ALTER TABLE "pieces" + ADD CONSTRAINT "pieces_constructeurId_fkey" + FOREIGN KEY ("constructeurId") REFERENCES "constructeurs"("id") + ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index d64a99b..4bc3f93 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -81,7 +81,6 @@ model Machine { id String @id @default(cuid()) name String reference String? - constructeur String? prix Decimal? @db.Decimal(10, 2) emplacement String? createdAt DateTime @default(now()) @@ -94,6 +93,9 @@ model Machine { typeMachineId String? typeMachine TypeMachine? @relation(fields: [typeMachineId], references: [id]) + constructeurId String? + constructeur Constructeur? @relation(fields: [constructeurId], references: [id], onDelete: SetNull) + composants Composant[] pieces Piece[] documents Document[] @relation("MachineDocuments") @@ -106,7 +108,6 @@ model Composant { id String @id @default(cuid()) name String reference String? - constructeur String? prix Decimal? @db.Decimal(10, 2) emplacement String? createdAt DateTime @default(now()) @@ -123,6 +124,9 @@ model Composant { typeComposantId String? typeComposant TypeComposant? @relation(fields: [typeComposantId], references: [id]) + constructeurId String? + constructeur Constructeur? @relation(fields: [constructeurId], references: [id], onDelete: SetNull) + pieces Piece[] documents Document[] @relation("ComposantDocuments") customFieldValues CustomFieldValue[] @relation("ComposantCustomFieldValues") @@ -134,7 +138,6 @@ model Piece { id String @id @default(cuid()) name String reference String? - constructeur String? prix Decimal? @db.Decimal(10, 2) emplacement String? createdAt DateTime @default(now()) @@ -150,12 +153,30 @@ model Piece { typePieceId String? typePiece TypePiece? @relation(fields: [typePieceId], references: [id]) + constructeurId String? + constructeur Constructeur? @relation(fields: [constructeurId], references: [id], onDelete: SetNull) + documents Document[] @relation("PieceDocuments") customFieldValues CustomFieldValue[] @relation("PieceCustomFieldValues") @@map("pieces") } +model Constructeur { + id String @id @default(cuid()) + name String @unique + email String? + phone String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + machines Machine[] + composants Composant[] + pieces Piece[] + + @@map("constructeurs") +} + model Document { id String @id @default(cuid()) name String diff --git a/src/app.module.ts b/src/app.module.ts index d4e1f86..8c03ab1 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -10,6 +10,7 @@ import { PiecesModule } from './pieces/pieces.module'; import { DocumentsModule } from './documents/documents.module'; import { TypesModule } from './types/types.module'; import { CustomFieldsModule } from './custom-fields/custom-fields.module'; +import { ConstructeursModule } from './constructeurs/constructeurs.module'; @Module({ imports: [ @@ -24,6 +25,7 @@ import { CustomFieldsModule } from './custom-fields/custom-fields.module'; DocumentsModule, TypesModule, CustomFieldsModule, + ConstructeursModule, ], controllers: [AppController], providers: [AppService], diff --git a/src/composants/composants.service.ts b/src/composants/composants.service.ts index aea948f..d9b2cc1 100644 --- a/src/composants/composants.service.ts +++ b/src/composants/composants.service.ts @@ -13,6 +13,7 @@ export class ComposantsService { machine: true, parentComposant: true, typeComposant: true, + constructeur: true, sousComposants: { include: { typeComposant: true, @@ -31,6 +32,7 @@ export class ComposantsService { machine: true, parentComposant: true, typeComposant: true, + constructeur: true, customFieldValues: { include: { customField: true, @@ -51,8 +53,10 @@ export class ComposantsService { customField: true, }, }, + constructeur: true, }, }, + constructeur: true, }, }, pieces: { @@ -62,6 +66,7 @@ export class ComposantsService { customField: true, }, }, + constructeur: true, }, }, documents: true, @@ -76,6 +81,7 @@ export class ComposantsService { machine: true, parentComposant: true, typeComposant: true, + constructeur: true, customFieldValues: { include: { customField: true, @@ -96,8 +102,10 @@ export class ComposantsService { customField: true, }, }, + constructeur: true, }, }, + constructeur: true, }, }, pieces: { @@ -107,6 +115,7 @@ export class ComposantsService { customField: true, }, }, + constructeur: true, }, }, documents: true, @@ -121,10 +130,12 @@ export class ComposantsService { machine: true, parentComposant: true, typeComposant: true, + constructeur: true, sousComposants: { include: { typeComposant: true, pieces: true, + constructeur: true, }, }, pieces: { @@ -134,6 +145,7 @@ export class ComposantsService { customField: true, }, }, + constructeur: true, }, }, customFieldValues: { @@ -155,6 +167,7 @@ export class ComposantsService { }, include: { typeComposant: true, + constructeur: true, customFieldValues: { include: { customField: true, @@ -163,6 +176,7 @@ export class ComposantsService { sousComposants: { include: { typeComposant: true, + constructeur: true, customFieldValues: { include: { customField: true, @@ -175,11 +189,13 @@ export class ComposantsService { customField: true, }, }, + constructeur: true, }, }, sousComposants: { include: { typeComposant: true, + constructeur: true, customFieldValues: { include: { customField: true, @@ -192,6 +208,7 @@ export class ComposantsService { customField: true, }, }, + constructeur: true, }, }, }, @@ -205,6 +222,7 @@ export class ComposantsService { customField: true, }, }, + constructeur: true, }, }, }, @@ -221,6 +239,7 @@ export class ComposantsService { machine: true, parentComposant: true, typeComposant: true, + constructeur: true, customFieldValues: { include: { customField: true, @@ -229,6 +248,7 @@ export class ComposantsService { sousComposants: { include: { typeComposant: true, + constructeur: true, customFieldValues: { include: { customField: true, @@ -241,6 +261,7 @@ export class ComposantsService { customField: true, }, }, + constructeur: true, }, }, }, @@ -252,6 +273,7 @@ export class ComposantsService { customField: true, }, }, + constructeur: true, }, }, documents: true, diff --git a/src/constructeurs/constructeurs.controller.ts b/src/constructeurs/constructeurs.controller.ts new file mode 100644 index 0000000..8aa0a00 --- /dev/null +++ b/src/constructeurs/constructeurs.controller.ts @@ -0,0 +1,33 @@ +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 { + constructor(private readonly constructeursService: ConstructeursService) {} + + @Post() + create(@Body() payload: CreateConstructeurDto) { + return this.constructeursService.create(payload) + } + + @Get() + findAll(@Query('search') search?: string) { + return this.constructeursService.findAll(search) + } + + @Get(':id') + findOne(@Param('id') id: string) { + return this.constructeursService.findOne(id) + } + + @Patch(':id') + update(@Param('id') id: string, @Body() payload: UpdateConstructeurDto) { + return this.constructeursService.update(id, payload) + } + + @Delete(':id') + remove(@Param('id') id: string) { + return this.constructeursService.remove(id) + } +} diff --git a/src/constructeurs/constructeurs.module.ts b/src/constructeurs/constructeurs.module.ts new file mode 100644 index 0000000..80256e7 --- /dev/null +++ b/src/constructeurs/constructeurs.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common' +import { ConstructeursService } from './constructeurs.service' +import { ConstructeursController } from './constructeurs.controller' + +@Module({ + controllers: [ConstructeursController], + providers: [ConstructeursService], + exports: [ConstructeursService], +}) +export class ConstructeursModule {} diff --git a/src/constructeurs/constructeurs.service.ts b/src/constructeurs/constructeurs.service.ts new file mode 100644 index 0000000..6c46c87 --- /dev/null +++ b/src/constructeurs/constructeurs.service.ts @@ -0,0 +1,64 @@ +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 {} + return { + OR: [ + { name: { contains: term, mode: 'insensitive' } }, + { email: { contains: term, mode: 'insensitive' } }, + { phone: { contains: term, mode: 'insensitive' } }, + ], + } + } + + async create(data: CreateConstructeurDto) { + return this.prisma.constructeur.create({ + data: { + ...data, + name: data.name.trim(), + 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) { + return this.prisma.constructeur.update({ + where: { id }, + data: { + ...data, + name: data.name?.trim(), + email: data.email?.trim(), + phone: data.phone?.trim(), + }, + }) + } + + async remove(id: string) { + return this.prisma.constructeur.delete({ + where: { id }, + }) + } +} diff --git a/src/machines/machines.service.ts b/src/machines/machines.service.ts index d8ea5c9..d735e05 100644 --- a/src/machines/machines.service.ts +++ b/src/machines/machines.service.ts @@ -27,6 +27,7 @@ export class MachinesService { include: { site: true, typeMachine: true, + constructeur: true, }, }); @@ -57,6 +58,7 @@ export class MachinesService { customFields: true, }, }, + constructeur: true, composants: { include: { typeComposant: true, @@ -68,8 +70,10 @@ export class MachinesService { customField: true, }, }, + constructeur: true, }, }, + constructeur: true, }, }, pieces: { @@ -79,6 +83,7 @@ export class MachinesService { customField: true, }, }, + constructeur: true, }, }, customFieldValues: { @@ -133,7 +138,7 @@ export class MachinesService { data: { name: component.name, reference: component.reference || '', - constructeur: component.constructeur || '', + constructeurId: await this.resolveConstructeurId(prisma, component.constructeur), emplacement: component.emplacement || '', prix: component.prix || null, machineId, @@ -202,7 +207,7 @@ export class MachinesService { data: { name: piece.name, reference: piece.reference || '', - constructeur: piece.constructeur || '', + constructeurId: await this.resolveConstructeurId(prisma, piece.constructeur), emplacement: piece.emplacement || '', prix: piece.prix || null, composantId: createdComposant.id, @@ -245,6 +250,7 @@ export class MachinesService { data: { name: piece.name, machineId, + constructeurId: await this.resolveConstructeurId(prisma, piece.constructeur), }, }); @@ -312,6 +318,7 @@ export class MachinesService { customFields: true, }, }, + constructeur: true, composants: { include: { typeComposant: true, @@ -321,6 +328,7 @@ export class MachinesService { customField: true, }, }, + constructeur: true, pieces: { include: { customFieldValues: { @@ -328,6 +336,7 @@ export class MachinesService { customField: true, }, }, + constructeur: true, }, }, }, @@ -339,6 +348,7 @@ export class MachinesService { customField: true, }, }, + constructeur: true, }, }, customFieldValues: { @@ -361,6 +371,7 @@ export class MachinesService { customFields: true, }, }, + constructeur: true, composants: { include: { typeComposant: true, @@ -370,6 +381,7 @@ export class MachinesService { customField: true, }, }, + constructeur: true, pieces: { include: { customFieldValues: { @@ -377,6 +389,7 @@ export class MachinesService { customField: true, }, }, + constructeur: true, }, }, }, @@ -388,6 +401,7 @@ export class MachinesService { customField: true, }, }, + constructeur: true, }, }, customFieldValues: { @@ -411,14 +425,34 @@ export class MachinesService { customFields: true, }, }, + constructeur: true, composants: { include: { typeComposant: true, sousComposants: true, - pieces: true, + constructeur: true, + pieces: { + include: { + constructeur: true, + customFieldValues: { + include: { + customField: true, + }, + }, + }, + }, + }, + }, + pieces: { + include: { + constructeur: true, + customFieldValues: { + include: { + customField: true, + }, + }, }, }, - pieces: true, customFieldValues: { include: { customField: true, @@ -429,6 +463,29 @@ export class MachinesService { }); } + private async resolveConstructeurId(prisma: any, rawName?: string) { + if (!rawName) return null + const name = String(rawName).trim() + if (!name) return null + + const existing = await prisma.constructeur.findFirst({ + where: { + name: { + equals: name, + mode: 'insensitive', + }, + }, + }) + + if (existing) return existing.id + + const created = await prisma.constructeur.create({ + data: { name }, + }) + + return created.id + } + async remove(id: string) { // Vérifier que la machine existe const machine = await this.prisma.machine.findUnique({ diff --git a/src/pieces/pieces.service.ts b/src/pieces/pieces.service.ts index b6df1d7..073321f 100644 --- a/src/pieces/pieces.service.ts +++ b/src/pieces/pieces.service.ts @@ -14,6 +14,7 @@ export class PiecesService { composant: true, typePiece: true, documents: true, + constructeur: true, }, }); } @@ -25,6 +26,7 @@ export class PiecesService { composant: true, typePiece: true, documents: true, + constructeur: true, }, }); } @@ -37,6 +39,7 @@ export class PiecesService { composant: true, typePiece: true, documents: true, + constructeur: true, }, }); } @@ -49,6 +52,7 @@ export class PiecesService { composant: true, typePiece: true, documents: true, + constructeur: true, }, }); } @@ -61,6 +65,7 @@ export class PiecesService { composant: true, typePiece: true, documents: true, + constructeur: true, }, }); } @@ -74,6 +79,7 @@ export class PiecesService { composant: true, typePiece: true, documents: true, + constructeur: true, }, }); } diff --git a/src/shared/dto/composant.dto.ts b/src/shared/dto/composant.dto.ts index 3e0599a..69df66c 100644 --- a/src/shared/dto/composant.dto.ts +++ b/src/shared/dto/composant.dto.ts @@ -19,7 +19,7 @@ export class CreateComposantDto { @IsOptional() @IsString() - constructeur?: string; + constructeurId?: string; @IsOptional() @Transform(({ value }) => value === '' ? null : value) @@ -46,7 +46,7 @@ export class UpdateComposantDto { @IsOptional() @IsString() - constructeur?: string; + constructeurId?: string; @IsOptional() @Transform(({ value }) => value === '' ? null : value) diff --git a/src/shared/dto/constructeur.dto.ts b/src/shared/dto/constructeur.dto.ts new file mode 100644 index 0000000..756da9b --- /dev/null +++ b/src/shared/dto/constructeur.dto.ts @@ -0,0 +1,28 @@ +import { IsEmail, IsOptional, IsString } from 'class-validator' + +export class CreateConstructeurDto { + @IsString() + name: string + + @IsOptional() + @IsEmail() + email?: string + + @IsOptional() + @IsString() + phone?: string +} + +export class UpdateConstructeurDto { + @IsOptional() + @IsString() + name?: string + + @IsOptional() + @IsEmail() + email?: string + + @IsOptional() + @IsString() + phone?: string +} diff --git a/src/shared/dto/machine.dto.ts b/src/shared/dto/machine.dto.ts index 89a58a5..8640413 100644 --- a/src/shared/dto/machine.dto.ts +++ b/src/shared/dto/machine.dto.ts @@ -1,4 +1,4 @@ -import { IsString, IsOptional, IsNumber, IsDecimal } from 'class-validator'; +import { IsString, IsOptional, IsDecimal } from 'class-validator'; export class CreateMachineDto { @IsString() @@ -13,7 +13,7 @@ export class CreateMachineDto { @IsOptional() @IsString() - constructeur?: string; + constructeurId?: string; @IsOptional() @IsDecimal() @@ -39,7 +39,7 @@ export class UpdateMachineDto { @IsOptional() @IsString() - constructeur?: string; + constructeurId?: string; @IsOptional() @IsDecimal() diff --git a/src/shared/dto/piece.dto.ts b/src/shared/dto/piece.dto.ts index b1f07cf..2f42073 100644 --- a/src/shared/dto/piece.dto.ts +++ b/src/shared/dto/piece.dto.ts @@ -19,7 +19,7 @@ export class CreatePieceDto { @IsOptional() @IsString() - constructeur?: string; + constructeurId?: string; @IsOptional() @Transform(({ value }) => value === '' ? null : value) @@ -46,7 +46,7 @@ export class UpdatePieceDto { @IsOptional() @IsString() - constructeur?: string; + constructeurId?: string; @IsOptional() @Transform(({ value }) => value === '' ? null : value)