refactor: prepare multi-machine inventory associations

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

View File

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

View File

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