This repository has been archived on 2026-04-01. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Inventory_backend/src/pieces/pieces.service.ts

434 lines
11 KiB
TypeScript

import { ConflictException, Injectable } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import { CreatePieceDto, UpdatePieceDto } from '../shared/dto/piece.dto';
import { PieceModelStructureSchema } from '../shared/schemas/inventory';
import type { PieceModelStructure } from '../shared/types/inventory';
const PIECE_WITH_RELATIONS_INCLUDE = {
typePiece: {
include: {
pieceCustomFields: true,
},
},
constructeur: true,
documents: true,
customFieldValues: {
include: {
customField: true,
},
},
machineLinks: {
include: {
machine: true,
typeMachinePieceRequirement: true,
parentLink: true,
},
},
} as const;
@Injectable()
export class PiecesService {
constructor(private prisma: PrismaService) {}
private buildCreateInput(createPieceDto: CreatePieceDto): Prisma.PieceCreateInput {
const data: Prisma.PieceCreateInput = {
name: createPieceDto.name,
reference: createPieceDto.reference ?? null,
prix: createPieceDto.prix !== undefined ? createPieceDto.prix : null,
};
if (createPieceDto.constructeurId) {
data.constructeur = {
connect: { id: createPieceDto.constructeurId },
};
}
if (createPieceDto.typePieceId) {
data.typePiece = {
connect: { id: createPieceDto.typePieceId },
};
}
return data;
}
async create(createPieceDto: CreatePieceDto) {
try {
const created = await this.prisma.piece.create({
data: this.buildCreateInput(createPieceDto),
include: PIECE_WITH_RELATIONS_INCLUDE,
});
await this.applyPieceSkeleton({
pieceId: created.id,
typePiece: created.typePiece as PieceTypeWithSkeleton | null,
});
return this.prisma.piece.findUnique({
where: { id: created.id },
include: PIECE_WITH_RELATIONS_INCLUDE,
});
} catch (error) {
this.handlePrismaError(error);
}
}
async findAll() {
return this.prisma.piece.findMany({
include: PIECE_WITH_RELATIONS_INCLUDE,
orderBy: { name: 'asc' },
});
}
async findOne(id: string) {
return this.prisma.piece.findUnique({
where: { id },
include: PIECE_WITH_RELATIONS_INCLUDE,
});
}
async update(id: string, updatePieceDto: UpdatePieceDto) {
const data: Prisma.PieceUpdateInput = {};
if (updatePieceDto.name !== undefined) {
data.name = updatePieceDto.name;
}
if (updatePieceDto.reference !== undefined) {
data.reference = updatePieceDto.reference;
}
if (updatePieceDto.prix !== undefined) {
data.prix = updatePieceDto.prix;
}
if (updatePieceDto.constructeurId !== undefined) {
data.constructeur = updatePieceDto.constructeurId
? { connect: { id: updatePieceDto.constructeurId } }
: { disconnect: true };
}
if (updatePieceDto.typePieceId !== undefined) {
data.typePiece = updatePieceDto.typePieceId
? { connect: { id: updatePieceDto.typePieceId } }
: { disconnect: true };
}
try {
const updated = await this.prisma.piece.update({
where: { id },
data,
include: PIECE_WITH_RELATIONS_INCLUDE,
});
await this.applyPieceSkeleton({
pieceId: updated.id,
typePiece: updated.typePiece as PieceTypeWithSkeleton | null,
});
return this.prisma.piece.findUnique({
where: { id: updated.id },
include: PIECE_WITH_RELATIONS_INCLUDE,
});
} catch (error) {
this.handlePrismaError(error);
}
}
async remove(id: string) {
const [machineLinksCount, documentsCount, customFieldValuesCount] =
await Promise.all([
this.prisma.machinePieceLink.count({
where: { pieceId: id },
}),
this.prisma.document.count({
where: { pieceId: id },
}),
this.prisma.customFieldValue.count({
where: { pieceId: id },
}),
]);
const blockingReasons: string[] = [];
if (machineLinksCount > 0) {
blockingReasons.push(
`${machineLinksCount} liaison${machineLinksCount > 1 ? 's' : ''} machine`,
);
}
if (documentsCount > 0) {
blockingReasons.push(
`${documentsCount} document${documentsCount > 1 ? 's' : ''}`,
);
}
if (blockingReasons.length > 0) {
const messageParts = [
`Impossible de supprimer cette pièce car elle possède encore: ${blockingReasons.join(
', ',
)}.`,
];
if (customFieldValuesCount > 0) {
messageParts.push(
`Les ${customFieldValuesCount} valeur${
customFieldValuesCount > 1 ? 's' : ''
} de champ personnalisé seront supprimées automatiquement une fois ces éléments détachés.`,
);
}
throw new ConflictException(
`${messageParts.join(' ')} Supprimez ou détachez les éléments indiqués avant de réessayer.`,
);
}
if (customFieldValuesCount > 0) {
await this.prisma.customFieldValue.deleteMany({
where: { pieceId: id },
});
}
return this.prisma.piece.delete({
where: { id },
});
}
private async applyPieceSkeleton({
pieceId,
typePiece,
}: {
pieceId: string;
typePiece: PieceTypeWithSkeleton | null;
}) {
if (!typePiece?.id) {
return;
}
const skeleton = this.parsePieceSkeleton(
(typePiece as { pieceSkeleton?: Prisma.JsonValue | null } | null)
?.pieceSkeleton,
);
if (!skeleton) {
return;
}
const customFields = skeleton.customFields ?? [];
await this.ensurePieceCustomFieldDefinitions(typePiece.id, customFields);
await this.createPieceCustomFieldValues(
pieceId,
typePiece.id,
customFields,
);
}
private parsePieceSkeleton(value: unknown): PieceModelStructure | null {
if (!value) {
return null;
}
try {
return PieceModelStructureSchema.parse(value);
} catch (error) {
return null;
}
}
private async ensurePieceCustomFieldDefinitions(
typePieceId: string,
customFields: PieceModelStructure['customFields'],
) {
if (
!typePieceId ||
!Array.isArray(customFields) ||
customFields.length === 0
) {
return;
}
const existing = await this.prisma.customField.findMany({
where: { typePieceId },
select: { id: true, name: true },
});
const existingByName = new Map(
existing.map((field) => [
this.normalizeIdentifier(field.name) ?? field.name,
field.id,
]),
);
for (const field of customFields) {
if (!field) {
continue;
}
const name = this.normalizeIdentifier(field.name);
if (!name || existingByName.has(name)) {
continue;
}
const type = this.normalizeIdentifier(field.type) ?? 'text';
const required = Boolean(field.required);
const options = this.normalizeOptions(field);
const created = await this.prisma.customField.create({
data: {
name,
type,
required,
options,
typePieceId,
},
select: { id: true },
});
existingByName.set(name, created.id);
}
}
private async createPieceCustomFieldValues(
pieceId: string,
typePieceId: string,
customFields: PieceModelStructure['customFields'],
) {
if (
!typePieceId ||
!Array.isArray(customFields) ||
customFields.length === 0
) {
return;
}
const definitions = await this.prisma.customField.findMany({
where: { typePieceId },
select: { id: true, name: true },
});
if (definitions.length === 0) {
return;
}
const definitionMap = new Map(
definitions.map((field) => [
this.normalizeIdentifier(field.name) ?? field.name,
field.id,
]),
);
const existingValues = await this.prisma.customFieldValue.findMany({
where: { pieceId },
select: { customFieldId: true },
});
const existingIds = new Set(
existingValues.map((value) => value.customFieldId),
);
for (const field of customFields) {
if (!field) {
continue;
}
const name = this.normalizeIdentifier(field.name);
if (!name) {
continue;
}
const definitionId = definitionMap.get(name);
if (!definitionId || existingIds.has(definitionId)) {
continue;
}
await this.prisma.customFieldValue.create({
data: {
customFieldId: definitionId,
pieceId,
value: this.toCustomFieldValue(field.value),
},
});
existingIds.add(definitionId);
}
}
private normalizeOptions(
field: PieceCustomFieldEntry | undefined,
): string[] | undefined {
const rawOptions = field?.options;
if (Array.isArray(rawOptions)) {
const normalized = rawOptions
.map((option) => (typeof option === 'string' ? option.trim() : ''))
.filter((option) => option.length > 0);
return normalized.length > 0 ? normalized : undefined;
}
const optionsTextValue =
field !== undefined
? (field as unknown as { optionsText?: unknown }).optionsText
: undefined;
if (typeof optionsTextValue === 'string') {
const normalized = optionsTextValue
.split(/\r?\n/)
.map((option: string) => option.trim())
.filter((option: string) => option.length > 0);
return normalized.length > 0 ? normalized : undefined;
}
return undefined;
}
private normalizeIdentifier(value: unknown): string | null {
if (typeof value !== 'string') {
return null;
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : null;
}
private toCustomFieldValue(value: unknown): string {
if (value === undefined || value === null) {
return '';
}
if (typeof value === 'string') {
return value;
}
return JSON.stringify(value);
}
private handlePrismaError(error: unknown): never {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
if (error.code === 'P2002' && this.isNameConstraint(error)) {
throw new ConflictException('Une pièce avec ce nom existe déjà.');
}
}
throw error;
}
private isNameConstraint(error: Prisma.PrismaClientKnownRequestError) {
const { target } = error.meta ?? {};
if (Array.isArray(target)) {
return target.includes('name');
}
if (typeof target === 'string') {
return target === 'name';
}
return false;
}
}
type PieceTypeWithSkeleton = Prisma.ModelTypeGetPayload<{
include: { pieceCustomFields: true };
}>;
type PieceCustomFieldEntry = NonNullable<PieceModelStructure['customFields']>[number];