diff --git a/README.md b/README.md index f2f9780..abf654d 100644 --- a/README.md +++ b/README.md @@ -101,48 +101,35 @@ Site → Machine → Composant → Sous-composant → ... - `PATCH /api/machines/:id` - Modifier une machine - `DELETE /api/machines/:id` - Supprimer une machine -#### Payloads `componentSelections` / `pieceSelections` +#### Payloads `componentLinks` / `pieceLinks` -Lors de la création d'une machine à partir d'un type, il est possible de fournir des sélections de composants et de pièces qui viendront remplir les exigences définies dans le type de machine. +Lors de la création ou de la reconfiguration d'une machine, il faut transmettre les liens qui associent la machine aux composants et pièces existants en respectant les exigences du type de machine. ```json { "name": "Presse HP-2000", "siteId": "", "typeMachineId": "", - "componentSelections": [ + "componentLinks": [ { "requirementId": "", - "typeComposantId": "", - "definition": { + "composantId": "", + "parentLinkId": "", + "overrides": { "name": "Bloc moteur série X", "reference": "COMP-001", - "prix": "12000.00", - "customFields": [ - { - "name": "Puissance nominale", - "type": "text", - "required": true, - "value": "7 kW" - } - ] + "prix": "12000.00" } } ], - "pieceSelections": [ + "pieceLinks": [ { "requirementId": "", - "typePieceId": "", - "definition": { + "pieceId": "", + "parentLinkId": "", + "overrides": { "name": "Kit maintenance niveau 1", - "reference": "KIT-001", - "customFields": [ - { - "name": "Référence fournisseur", - "type": "text", - "value": "STD-002" - } - ] + "reference": "KIT-001" } } ] @@ -152,10 +139,10 @@ Lors de la création d'une machine à partir d'un type, il est possible de fourn Principales règles de validation : - `requirementId` doit correspondre à une exigence déclarée dans le type de machine (composant ou pièce). -- Le nombre de sélections pour une exigence doit respecter `minCount` et `maxCount` (si défini). Les exigences marquées `required` imposent au moins une sélection. -- Si `allowNewModels` vaut `false`, la sélection doit réutiliser un composant ou une pièce existante et respecter strictement le type imposé par le requirement. Les squelettes définis sur les types sont instanciés automatiquement lors de la création. -- Les modèles sélectionnés doivent appartenir au type attendu (`typeComposantId` ou `typePieceId`) sous peine d'échec de la création. -- Les champs personnalisés du `definition.customFields` permettent de surcharger la valeur par défaut définie au niveau du type; la valeur est automatiquement injectée dans les `customFieldValues` de la machine, du composant ou de la pièce créée. +- Le nombre de liens fournis pour une exigence doit respecter `minCount` et `maxCount` (si défini). Les exigences marquées `required` imposent au moins un lien. +- Les composants et pièces liés doivent être compatibles avec le type attendu (`typeComposantId` ou `typePieceId`). +- `parentLinkId` permet de rattacher un composant ou une pièce à un lien parent déjà créé pour la même machine. +- Les `overrides` sont optionnels et permettent de surcharger le nom, la référence ou le prix affichés sur la machine sans modifier l'élément d'origine. ### Composants - `GET /api/composants` - Liste tous les composants diff --git a/package-lock.json b/package-lock.json index efc70d6..9488ee9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -243,7 +243,6 @@ "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -2287,7 +2286,6 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -2456,7 +2454,6 @@ "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.5.tgz", "integrity": "sha512-DQpWdr3ShO0BHWkHl3I4W/jR6R3pDtxyBlmrpTuZF+PXxQyBXNvsUne0Wyo6QHPEDi+pAz9XchBFoKbqOhcdTg==", "license": "MIT", - "peer": true, "dependencies": { "file-type": "21.0.0", "iterare": "1.2.1", @@ -2504,7 +2501,6 @@ "integrity": "sha512-Qr25MEY9t8VsMETy7eXQ0cNXqu0lzuFrrTr+f+1G57ABCtV5Pogm7n9bF71OU2bnkDD32Bi4hQLeFR90cku3Tw==", "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "@nuxt/opencollective": "0.4.1", "fast-safe-stringify": "2.1.1", @@ -2565,7 +2561,6 @@ "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.5.tgz", "integrity": "sha512-OsoiUBY9Shs5IG3uvDIt9/IDfY5OlvWBESuB/K4Eun8xILw1EK5d5qMfC3d2sIJ+kA3l+kBR1d/RuzH7VprLIg==", "license": "MIT", - "peer": true, "dependencies": { "cors": "2.8.5", "express": "5.1.0", @@ -2949,7 +2944,6 @@ "integrity": "sha512-Q5FsI3Cw0fGMXhmsg7c08i4EmXCrcl+WnAxb6LYOLHw4JFFC3yzmx9LaXZ7QMbA+JZXbigU2TirI7RAfO0Qlnw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@swc/counter": "^0.1.3", "@xhmikosr/bin-wrapper": "^13.0.5", @@ -3022,7 +3016,6 @@ "dev": true, "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.23" @@ -3386,7 +3379,6 @@ "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -3534,7 +3526,6 @@ "integrity": "sha512-bJFoMATwIGaxxx8VJPeM8TonI8t579oRvgAuT8zFugJsJZgzqv0Fu8Mhp68iecjzG7cnN3mO2dJQ5uUM2EFrgQ==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -3676,7 +3667,6 @@ "integrity": "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.38.0", "@typescript-eslint/types": "8.38.0", @@ -4355,7 +4345,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "devOptional": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4369,6 +4358,7 @@ "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10.13.0" }, @@ -4405,7 +4395,6 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -4929,7 +4918,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", @@ -5184,7 +5172,6 @@ "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "readdirp": "^4.0.1" }, @@ -5232,15 +5219,13 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/class-validator": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.2.tgz", "integrity": "sha512-3kMVRF2io8N8pY1IFIXlho9r8IPUUIfHe2hYVtiebvAzU2XeQFXTv+XI4WX+TnXmtwXMDcjngcpkiPM0O9PvLw==", "license": "MIT", - "peer": true, "dependencies": { "@types/validator": "^13.11.8", "libphonenumber-js": "^1.11.1", @@ -6020,7 +6005,6 @@ "integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -6082,7 +6066,6 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -7707,7 +7690,6 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -9380,7 +9362,6 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", "license": "MIT", - "peer": true, "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", @@ -9648,7 +9629,6 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -9706,7 +9686,6 @@ "integrity": "sha512-pmV7NEqQej9WjizN6RSNIwf7Y+jeh9mY1JEX2WjGxJi4YZWexClhde1yz/FuvAM+cTwzchcMytu2m4I6wPkIzg==", "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@prisma/config": "6.12.0", "@prisma/engines": "6.12.0" @@ -9911,8 +9890,7 @@ "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/repeat-string": { "version": "1.6.1", @@ -10107,7 +10085,6 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -10961,7 +10938,6 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -11312,7 +11288,6 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -11473,7 +11448,6 @@ "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.25.tgz", "integrity": "sha512-fTKDFzWXKwAaBdEMU4k661seZewbNYET4r1J/z3Jwf+eAvlzMVpTLKAVcAzg75WwQk7GDmtsmkZ5MfkmXCiFWg==", "license": "MIT", - "peer": true, "dependencies": { "@sqltools/formatter": "^1.2.5", "ansis": "^3.17.0", @@ -11686,7 +11660,6 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12033,6 +12006,7 @@ "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ajv": "^8.0.0" }, @@ -12051,6 +12025,7 @@ "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3" }, @@ -12064,6 +12039,7 @@ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -12078,6 +12054,7 @@ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "engines": { "node": ">=4.0" } @@ -12087,7 +12064,8 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/webpack/node_modules/mime-db": { "version": "1.52.0", @@ -12095,6 +12073,7 @@ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.6" } @@ -12105,6 +12084,7 @@ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "mime-db": "1.52.0" }, @@ -12118,6 +12098,7 @@ "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", diff --git a/package.json b/package.json index d8a3d79..dd5ba82 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "test:e2e": "jest --config ./test/jest-e2e.json", - "seed:demo": "ts-node --transpile-only scripts/seed-industrial-data.ts" + "seed:demo": "ts-node --transpile-only scripts/seed-industrial-data.ts", + "seed:sample": "ts-node --transpile-only scripts/seed-sample-data.ts" }, "dependencies": { "@nestjs/common": "^11.0.1", diff --git a/prisma/migrations/20250927120000_add_component_structure/migration.sql b/prisma/migrations/20250927120000_add_component_structure/migration.sql new file mode 100644 index 0000000..58c9b86 --- /dev/null +++ b/prisma/migrations/20250927120000_add_component_structure/migration.sql @@ -0,0 +1,3 @@ +-- Add JSON column to store instantiated structure selections on components +ALTER TABLE "composants" +ADD COLUMN IF NOT EXISTS "structure" JSONB; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 230ecce..5f35420 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -84,6 +84,7 @@ model Composant { prix Decimal? @db.Decimal(10, 2) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + structure Json? typeComposantId String? typeComposant ModelType? @relation("ModelTypeComponentAssignments", fields: [typeComposantId], references: [id]) diff --git a/scripts/cleanup-custom-fields.ts b/scripts/cleanup-custom-fields.ts deleted file mode 100644 index 7255dfd..0000000 --- a/scripts/cleanup-custom-fields.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { PrismaClient } from '@prisma/client' - -const prisma = new PrismaClient() - -async function main () { - try { - console.log('Starting custom fields cleanup...') - - const deletedValues = await prisma.customFieldValue.deleteMany({ - where: { - customField: { - OR: [ - { typeComposantId: { not: null } }, - { typePieceId: { not: null } }, - { typeMachineId: { not: null } } - ] - } - } - }) - console.log(`Deleted ${deletedValues.count} custom field values linked to type-level definitions.`) - - const deletedFields = await prisma.customField.deleteMany({ - where: { - OR: [ - { typeComposantId: { not: null } }, - { typePieceId: { not: null } }, - { typeMachineId: { not: null } } - ] - } - }) - console.log(`Deleted ${deletedFields.count} custom field definitions linked to model types.`) - - console.log('Cleanup complete.') - } catch (error) { - console.error('Cleanup failed:', error) - process.exitCode = 1 - } finally { - await prisma.$disconnect() - } -} - -main() diff --git a/scripts/seed-basic-categories.ts b/scripts/seed-basic-categories.ts deleted file mode 100644 index c886bb5..0000000 --- a/scripts/seed-basic-categories.ts +++ /dev/null @@ -1,593 +0,0 @@ -import { Prisma, PrismaClient, ModelCategory } from '@prisma/client'; - -const prisma = new PrismaClient(); - -type CustomFieldInput = { - name: string; - type: 'text' | 'number' | 'select'; - required?: boolean; - options?: readonly string[]; -}; - -type ModelTypeSeed = { - code: string; - name: string; - description: string; - customFields: readonly CustomFieldInput[]; -}; - -type ComponentRequirementSeed = { - typeCode: string; - label: string; - minCount: number; - maxCount?: number | null; - required?: boolean; - allowNewModels?: boolean; -}; - -type PieceRequirementSeed = { - typeCode: string; - label: string; - minCount: number; - maxCount?: number | null; - required?: boolean; - allowNewModels?: boolean; -}; - -const componentTypes: readonly ModelTypeSeed[] = [ - { - code: 'drive-module', - name: 'Module d entrainement', - description: 'Sous-ensemble moteur et reducteur pour entrainements principaux.', - customFields: [ - { name: 'Puissance nominale (kW)', type: 'number', required: true }, - { name: 'Indice de protection', type: 'select', options: ['IP55', 'IP65', 'IP66'] }, - ], - }, - { - code: 'sensor-array', - name: 'Chaine de capteurs', - description: 'Groupe de capteurs industriels (temperature, vibration, debit).', - customFields: [ - { name: 'Type principal', type: 'select', options: ['Temperature', 'Vibration', 'Debit'] }, - { name: 'Plage de mesure', type: 'text' }, - ], - }, - { - code: 'control-cabinet', - name: 'Armoire de controle', - description: 'Armoire electrique avec automate, protection et distribution.', - customFields: [ - { name: 'Tension alimentation (V)', type: 'number' }, - { name: 'Nombre de departs', type: 'number' }, - ], - }, - { - code: 'hydraulic-pack', - name: 'Groupe hydraulique', - description: 'Bloc hydraulique complet (pompes, accumulateurs, filtration).', - customFields: [ - { name: 'Pression nominale (bar)', type: 'number', required: true }, - { name: 'Debit nominal (L/min)', type: 'number' }, - ], - }, - { - code: 'structure-frame', - name: 'Chassis structurel', - description: 'Structure porteuse ou chassis mecano-soude.', - customFields: [ - { name: 'Matiere', type: 'select', options: ['Acier', 'Inox', 'Aluminium'] }, - { name: 'Charge admissible (kg)', type: 'number' }, - ], - }, -]; - -const pieceTypes: readonly ModelTypeSeed[] = [ - { - code: 'belt-kit', - name: 'Kit courroie', - description: 'Courroie et accessoires pour entrainements.', - customFields: [ - { name: 'Type', type: 'select', options: ['Poly-V', 'Trapezoidale', 'Synchronisee'] }, - { name: 'Longueur (mm)', type: 'number' }, - ], - }, - { - code: 'bearing-set', - name: 'Jeu de roulements', - description: 'Paire de roulements avec bagues et graisse.', - customFields: [ - { name: 'Diametre interieur (mm)', type: 'number', required: true }, - { name: 'Classe', type: 'select', options: ['P0', 'P6', 'P5'] }, - ], - }, - { - code: 'filter-cartridge', - name: 'Cartouche filtrante', - description: 'Element filtrant pour fluides ou air.', - customFields: [ - { name: 'Grade de filtration (um)', type: 'number' }, - { name: 'Type de media', type: 'select', options: ['Cellulose', 'Synthetique', 'Inox'] }, - ], - }, - { - code: 'sensor-probe', - name: 'Sonde de mesure', - description: 'Sonde ou capteur unitaire avec cable.', - customFields: [ - { name: 'Signal de sortie', type: 'select', options: ['4-20 mA', '0-10 V', 'PT100'] }, - { name: 'Indice IP', type: 'select', options: ['IP67', 'IP68'] }, - ], - }, - { - code: 'maintenance-kit', - name: 'Kit maintenance', - description: 'Ensemble de pieces pour maintenance planifiee.', - customFields: [ - { name: 'Niveau de maintenance', type: 'select', options: ['Preventif', 'Correctif', 'Lourde'] }, - { name: 'Duree estimee (h)', type: 'number' }, - ], - }, -]; - -const constructors = [ - { name: 'ElectroMec Industrie', email: 'contact@electromec.fr', phone: '+33 4 72 00 11 22' }, - { name: 'Hydraulic Systems Europe', email: 'sales@hydraulics-eu.com', phone: '+33 5 56 12 34 56' }, - { name: 'Automation Lyon', email: 'support@automation-lyon.fr', phone: '+33 4 37 50 60 70' }, - { name: 'ThermoTech Solutions', email: 'info@thermotech.eu', phone: '+33 1 44 55 66 77' }, - { name: 'BearingWorks', email: 'service@bearingworks.com', phone: '+33 3 88 90 12 45' }, -] as const; - -const machineCustomFields: readonly CustomFieldInput[] = [ - { name: 'Reference installation', type: 'text' }, - { name: 'Puissance installee (kW)', type: 'number' }, - { name: 'Zone critique', type: 'select', options: ['Zone A', 'Zone B', 'Zone C'] }, -]; - -const componentRequirementSeeds: readonly ComponentRequirementSeed[] = [ - { - typeCode: 'structure-frame', - label: 'Chassis principal', - minCount: 1, - maxCount: 1, - required: true, - allowNewModels: false, - }, - { - typeCode: 'drive-module', - label: 'Module d entrainement principal', - minCount: 1, - maxCount: 1, - required: true, - allowNewModels: false, - }, - { - typeCode: 'control-cabinet', - label: 'Armoire de controle', - minCount: 1, - maxCount: 1, - required: true, - allowNewModels: false, - }, - { - typeCode: 'sensor-array', - label: 'Capteurs de surveillance', - minCount: 1, - maxCount: 3, - required: true, - allowNewModels: true, - }, - { - typeCode: 'hydraulic-pack', - label: 'Groupe hydraulique auxiliaire', - minCount: 0, - maxCount: 1, - required: false, - allowNewModels: true, - }, -]; - -const pieceRequirementSeeds: readonly PieceRequirementSeed[] = [ - { - typeCode: 'belt-kit', - label: 'Kit courroie de rechange', - minCount: 1, - maxCount: 2, - required: true, - allowNewModels: true, - }, - { - typeCode: 'bearing-set', - label: 'Roulements de secours', - minCount: 1, - maxCount: 2, - required: true, - allowNewModels: true, - }, - { - typeCode: 'filter-cartridge', - label: 'Cartouches de filtration', - minCount: 0, - maxCount: 4, - required: false, - allowNewModels: true, - }, - { - typeCode: 'maintenance-kit', - label: 'Kit maintenance planifiee', - minCount: 0, - maxCount: 1, - required: false, - allowNewModels: true, - }, - { - typeCode: 'sensor-probe', - label: 'Sondes de rechange', - minCount: 1, - maxCount: 4, - required: true, - allowNewModels: true, - }, -]; - -function mapCustomFields(fields: readonly CustomFieldInput[]) { - if (!fields.length) { - return undefined; - } - return { - create: fields.map((field) => ({ - name: field.name, - type: field.type, - required: field.required ?? false, - options: field.options ? [...field.options] : [], - })), - } as const; -} - -async function upsertComponentType(type: ModelTypeSeed) { - const customFields = mapCustomFields(type.customFields); - await prisma.modelType.upsert({ - where: { code: type.code }, - update: { - name: type.name, - description: type.description, - notes: type.description, - customFields: { - deleteMany: {}, - ...(customFields ?? {}), - }, - }, - create: { - code: type.code, - name: type.name, - description: type.description, - notes: type.description, - category: ModelCategory.COMPONENT, - ...(customFields ? { customFields } : {}), - }, - }); -} - -async function upsertPieceType(type: ModelTypeSeed) { - const customFields = mapCustomFields(type.customFields); - await prisma.modelType.upsert({ - where: { code: type.code }, - update: { - name: type.name, - description: type.description, - notes: type.description, - pieceCustomFields: { - deleteMany: {}, - ...(customFields ?? {}), - }, - }, - create: { - code: type.code, - name: type.name, - description: type.description, - notes: type.description, - category: ModelCategory.PIECE, - ...(customFields ? { pieceCustomFields: customFields } : {}), - }, - }); -} - -async function applyPieceSkeletons(pieceMap: Map) { - type PieceSkeleton = { - customFields?: Array<{ name: string; value?: unknown; type?: string; required?: boolean; options?: unknown }>; - [key: string]: unknown; - }; - - const definitions: Record = { - 'belt-kit': { - customFields: [ - { name: 'Type', value: 'Poly-V' }, - { name: 'Longueur (mm)', value: 1800 }, - ], - remplacementHeures: 1500, - stockageRecommande: 'Local sec et tempere', - }, - 'bearing-set': { - customFields: [ - { name: 'Diametre interieur (mm)', value: 45 }, - { name: 'Classe', value: 'P6' }, - ], - graisseRecommandee: 'Lithium NLGI2', - }, - 'filter-cartridge': { - customFields: [ - { name: 'Grade de filtration (um)', value: 10 }, - { name: 'Type de media', value: 'Synthetique' }, - ], - remplacementMensuel: true, - }, - 'sensor-probe': { - customFields: [ - { name: 'Signal de sortie', value: '4-20 mA' }, - { name: 'Indice IP', value: 'IP67' }, - ], - calibrationIntervalJours: 180, - }, - 'maintenance-kit': { - customFields: [ - { name: 'Niveau de maintenance', value: 'Preventif' }, - { name: 'Duree estimee (h)', value: 4 }, - ], - contenuStandard: ['Filtres', 'Joints', 'Visserie'], - }, - }; - - for (const [code, structure] of Object.entries(definitions)) { - const record = pieceMap.get(code); - if (!record) { - continue; - } - - await prisma.modelType.update({ - where: { id: record.id }, - data: { - pieceSkeleton: structure as Prisma.InputJsonValue, - }, - }); - } -} - -async function applyComponentSkeletons( - componentMap: Map, - pieceMap: Map, -) { - const pieceRef = (code: string, role?: string) => { - const piece = pieceMap.get(code); - if (!piece) { - throw new Error(`Piece type ${code} requis pour le squelette`); - } - return { - typePieceId: piece.id, - ...(role ? { role } : {}), - }; - }; - - const componentRef = (code: string, alias?: string) => { - const component = componentMap.get(code); - if (!component) { - throw new Error(`Component type ${code} requis pour le squelette`); - } - return { - typeComposantId: component.id, - ...(alias ? { alias } : {}), - }; - }; - - type ComponentSkeleton = { - pieces: Array<{ typePieceId: string; role?: string }>; - customFields: Array<{ key: string; value: unknown }>; - subcomponents: Array<{ typeComposantId?: string; alias?: string; familyCode?: string; modelId?: string }>; - }; - - const definitions: Record = { - 'drive-module': { - pieces: [ - pieceRef('belt-kit', 'Courroie principale'), - pieceRef('bearing-set', 'Roulements de sortie'), - ], - customFields: [ - { key: 'Lubrification', value: 'Graissage centralise' }, - { key: 'ControleVibration', value: 'Capteurs integres' }, - ], - subcomponents: [componentRef('sensor-array', 'Capteurs integres')], - }, - 'sensor-array': { - pieces: [pieceRef('sensor-probe', 'Sonde principale')], - customFields: [ - { key: 'Calibration', value: 'A effectuer tous les 6 mois' }, - { key: 'NombreCapteursMax', value: 6 }, - ], - subcomponents: [], - }, - 'control-cabinet': { - pieces: [ - pieceRef('maintenance-kit', 'Kit rechange armoire'), - pieceRef('sensor-probe', 'Sonde ambiance'), - ], - customFields: [ - { key: 'ClassementLocal', value: 'Non ATEX' }, - { key: 'RefAutomate', value: 'PLC-STD-200' }, - ], - subcomponents: [], - }, - 'hydraulic-pack': { - pieces: [ - pieceRef('filter-cartridge', 'Filtre hydraulique'), - pieceRef('maintenance-kit', 'Kit joints hydrauliques'), - ], - customFields: [ - { key: 'ReservoirLitres', value: 120 }, - { key: 'TypeHuile', value: 'HLP46' }, - ], - subcomponents: [componentRef('sensor-array', 'Capteurs pression et debit')], - }, - 'structure-frame': { - pieces: [], - customFields: [ - { key: 'Revêtement', value: 'Peinture epoxy' }, - { key: 'PointsLevage', value: 4 }, - ], - subcomponents: [componentRef('sensor-array', 'Capteurs deformation')], - }, - }; - - for (const [code, structure] of Object.entries(definitions)) { - const record = componentMap.get(code); - if (!record) { - continue; - } - - await prisma.modelType.update({ - where: { id: record.id }, - data: { - componentSkeleton: structure as Prisma.InputJsonValue, - }, - }); - } -} - -function buildComponentRequirements( - componentMap: Map, - seeds: readonly ComponentRequirementSeed[], -) { - return seeds.map((seed) => { - const type = componentMap.get(seed.typeCode); - if (!type) { - throw new Error(`Type composant ${seed.typeCode} introuvable pour le requirement`); - } - return { - label: seed.label, - minCount: seed.minCount, - maxCount: seed.maxCount ?? null, - required: seed.required ?? true, - allowNewModels: seed.allowNewModels ?? true, - typeComposant: { connect: { id: type.id } }, - }; - }); -} - -function buildPieceRequirements( - pieceMap: Map, - seeds: readonly PieceRequirementSeed[], -) { - return seeds.map((seed) => { - const type = pieceMap.get(seed.typeCode); - if (!type) { - throw new Error(`Type piece ${seed.typeCode} introuvable pour le requirement`); - } - return { - label: seed.label, - minCount: seed.minCount, - maxCount: seed.maxCount ?? null, - required: seed.required ?? true, - allowNewModels: seed.allowNewModels ?? true, - typePiece: { connect: { id: type.id } }, - }; - }); -} - -async function seedMachineTemplate( - componentMap: Map, - pieceMap: Map, -) { - const name = 'Cellule Modulaire Standard'; - const description = 'Module generique compose d un chassis, d un entrainement, de capteurs et d une armoire de controle.'; - const componentRequirements = buildComponentRequirements(componentMap, componentRequirementSeeds); - const pieceRequirements = buildPieceRequirements(pieceMap, pieceRequirementSeeds); - - await prisma.typeMachine.upsert({ - where: { name }, - update: { - description, - category: 'Module', - maintenanceFrequency: 'Mensuelle', - customFields: { - deleteMany: {}, - ...(mapCustomFields(machineCustomFields) ?? {}), - }, - componentRequirements: { - deleteMany: {}, - create: componentRequirements, - }, - pieceRequirements: { - deleteMany: {}, - create: pieceRequirements, - }, - }, - create: { - name, - description, - category: 'Module', - maintenanceFrequency: 'Mensuelle', - ...(mapCustomFields(machineCustomFields) ? { customFields: mapCustomFields(machineCustomFields)! } : {}), - componentRequirements: { - create: componentRequirements, - }, - pieceRequirements: { - create: pieceRequirements, - }, - }, - }); -} - -async function main() { - console.log('Seeding component categories...'); - for (const component of componentTypes) { - await upsertComponentType(component); - } - - console.log('Seeding piece categories...'); - for (const piece of pieceTypes) { - await upsertPieceType(piece); - } - - const componentRecords = await prisma.modelType.findMany({ - where: { code: { in: componentTypes.map((type) => type.code) } }, - select: { id: true, code: true }, - }); - const pieceRecords = await prisma.modelType.findMany({ - where: { code: { in: pieceTypes.map((type) => type.code) } }, - select: { id: true, code: true }, - }); - - const componentMap = new Map(componentRecords.map((record) => [record.code, { id: record.id }])); - const pieceMap = new Map(pieceRecords.map((record) => [record.code, { id: record.id }])); - - console.log('Applying piece skeletons...'); - await applyPieceSkeletons(pieceMap); - - console.log('Applying component skeletons...'); - await applyComponentSkeletons(componentMap, pieceMap); - - console.log('Seeding constructors...'); - for (const constructeur of constructors) { - await prisma.constructeur.upsert({ - where: { name: constructeur.name }, - update: { - email: constructeur.email, - phone: constructeur.phone, - }, - create: constructeur, - }); - } - - console.log('Configuring machine template...'); - await seedMachineTemplate(componentMap, pieceMap); -} - -main() - .then(() => { - console.log('Seed completed.'); - }) - .catch((error) => { - console.error('Seed failed:', error); - process.exitCode = 1; - }) - .finally(async () => { - await prisma.$disconnect(); - }); diff --git a/scripts/seed-industrial-data.ts b/scripts/seed-industrial-data.ts deleted file mode 100644 index 50bba79..0000000 --- a/scripts/seed-industrial-data.ts +++ /dev/null @@ -1,4749 +0,0 @@ -import { PrismaClient, Prisma, ModelCategory } from '@prisma/client'; -import { normalizeComponentModelStructure } from '../src/component-models/structure.normalizer'; -import { - ComponentModelStructureSchema, - PieceModelStructureSchema, -} from '../src/shared/schemas/inventory'; -import type { ComponentModelStructure } from '../src/shared/types/inventory'; - -const prisma = new PrismaClient(); - -type CustomFieldSpec = { - name: string; - type: 'string' | 'number' | 'boolean' | 'date' | 'select'; - required?: boolean; - defaultValue?: string; - options?: string[]; -}; - -type ComponentTypeDefinition = { - code: string; - name: string; - description: string; - customFields: CustomFieldSpec[]; -}; - -type PieceTypeDefinition = { - code: string; - name: string; - description: string; - customFields: CustomFieldSpec[]; -}; - -type PieceModelDefinition = { - code: string; - name: string; - description: string; - typeCode: string; - structure?: Prisma.InputJsonValue; -}; - -type ComponentModelStructureDraft = - | ComponentModelStructure - | ({ - recommendedCustomFields?: Record; - customFields?: Array<{ key?: string; name?: string; value?: unknown }>; - pieceTemplates?: Array<{ - typeCode?: string; - modelCode?: string; - quantity?: number; - usage?: string; - notes?: string; - role?: string; - }>; - subComponentTemplates?: Array<{ - typeCode?: string; - suggestedModelCodes?: string[]; - alias?: string; - notes?: string; - }>; - } & Record); - -type ComponentModelDefinition = { - code: string; - name: string; - description: string; - typeCode: string; - structure?: ComponentModelStructureDraft | Prisma.InputJsonValue; -}; - -type ConstructeurDefinition = { - key: string; - name: string; - email?: string; - phone?: string; -}; - -type ComponentPieceInstance = { - name: string; - reference?: string; - prix?: string; - typeCode: string; - modelCode: string; - constructeur?: string; - customValues?: Record; -}; - -type ComponentInstance = { - name: string; - reference?: string; - prix?: string; - typeCode: string; - requirementLabel?: string; - modelCode: string; - constructeur?: string; - customValues?: Record; - pieces?: ComponentPieceInstance[]; - children?: ComponentInstance[]; -}; - -type TypeMachineDefinition = { - code: string; - name: string; - description: string; - category: string; - maintenanceFrequency: string; - specifications: Prisma.InputJsonValue; - customFields: CustomFieldSpec[]; - componentRequirements: { - label: string; - typeCode: string; - minCount: number; - maxCount?: number; - required: boolean; - }[]; - pieceRequirements?: { - label: string; - typeCode: string; - minCount: number; - maxCount?: number; - required: boolean; - }[]; -}; - -type MachineBuildSpec = { - code: string; - typeMachineCode: string; - name: string; - reference: string; - prix: string; - constructeurKey: string; - customFieldValues: Record; - components: ComponentInstance[]; - sparePieces?: (ComponentPieceInstance & { requirementLabel?: string })[]; -}; - -type TypeMachineRecord = Prisma.TypeMachineGetPayload<{ - include: { - customFields: true; - componentRequirements: true; - pieceRequirements: true; - }; -}>; - -const componentTypeDefinitions: ComponentTypeDefinition[] = [ - { - code: 'motor-drive', - name: 'Groupe moteur', - description: 'Motorisation asynchrone montée sur bride pour entraînements industriels.', - customFields: [ - { name: 'Puissance nominale (kW)', type: 'number', required: true }, - { - name: 'Classe énergétique', - type: 'select', - defaultValue: 'IE3', - options: ['IE2', 'IE3', 'IE4'], - }, - { - name: "Indice de protection", - type: 'select', - defaultValue: 'IP55', - options: ['IP55', 'IP65', 'IP23'], - }, - ], - }, - { - code: 'gearbox-assembly', - name: 'Train réducteur', - description: 'Réducteur industriel pour adaptation de vitesse.', - customFields: [ - { - name: 'Rapport de réduction', - type: 'select', - defaultValue: '1:25', - options: ['1:18', '1:24', '1:28', '1:32'], - }, - { name: 'Couple nominal (Nm)', type: 'number', required: true }, - { - name: 'Type de montage', - type: 'select', - options: ['À bride', 'À pattes', 'Sur arbre'], - defaultValue: 'À bride', - }, - ], - }, - { - code: 'bucket-head-section', - name: "Tête d'élévateur", - description: 'Module supérieur avec tambour moteur et trappes de visite.', - customFields: [ - { name: 'Largeur tambour (mm)', type: 'number', required: true }, - { - name: 'Type de revêtement', - type: 'select', - options: ['Caoutchouc rainuré', 'Céramique', 'Acier lisse'], - defaultValue: 'Caoutchouc rainuré', - }, - { name: 'Nombre de trappes', type: 'number' }, - ], - }, - { - code: 'bucket-boot-section', - name: "Pied d'élévateur", - description: 'Module inférieur avec trémie et organes de tension.', - customFields: [ - { name: 'Capacité trémie (L)', type: 'number', required: true }, - { - name: 'Mode de tension', - type: 'select', - options: ['Vis manuelle', 'Contrepoids', 'Hydraulique'], - defaultValue: 'Vis manuelle', - }, - { - name: 'Système de nettoyage', - type: 'select', - options: ['Grattoirs', 'Balais', 'Vide-pied'], - defaultValue: 'Grattoirs', - }, - ], - }, - { - code: 'bucket-leg-section', - name: 'Tronçon de gaine', - description: 'Section intermédiaire de gaine pour élévateur.', - customFields: [ - { name: 'Hauteur section (m)', type: 'number', required: true }, - { - name: 'Type de gaine', - type: 'select', - options: ['Boulonnée', 'Soudée', 'Renforcée'], - defaultValue: 'Boulonnée', - }, - ], - }, - { - code: 'belt-drive-station', - name: "Station d'entraînement", - description: 'Station de tête pour convoyeur à bande.', - customFields: [ - { - name: 'Type de tambour', - type: 'select', - options: ['Caoutchouc rainuré', 'Métallique', 'Céramique'], - defaultValue: 'Caoutchouc rainuré', - }, - { - name: 'Système de tension', - type: 'select', - options: ['Vis manuelle', 'Hydraulique', 'Pneumatique'], - defaultValue: 'Vis manuelle', - }, - ], - }, - { - code: 'belt-tail-station', - name: 'Station de retour', - description: 'Tambour de retour et système de nettoyage de bande.', - customFields: [ - { - name: 'Nettoyeur principal', - type: 'select', - options: ['Racleur PU', 'Brosse acier', 'Racleur secondaire'], - defaultValue: 'Racleur PU', - }, - { name: 'Diamètre tambour (mm)', type: 'number', required: true }, - ], - }, - { - code: 'belt-support-frame', - name: 'Ossature convoyeur', - description: 'Structure intermédiaire et rouleaux porteurs.', - customFields: [ - { name: 'Longueur (m)', type: 'number', required: true }, - { name: 'Nombre de rouleaux', type: 'number', required: true }, - { - name: 'Type de châssis', - type: 'select', - options: ['Treillis', 'Caisson', 'Portique'], - defaultValue: 'Treillis', - }, - ], - }, - { - code: 'gravity-vibration-deck', - name: 'Plateau vibrant', - description: 'Plateau densimétrique pour séparation des grains.', - customFields: [ - { - name: 'Type de plateau', - type: 'select', - options: ['Acier perforé', 'Inox poli', 'Composite'], - defaultValue: 'Acier perforé', - }, - { name: 'Fréquence de vibration (Hz)', type: 'number', required: true }, - { name: 'Inclinaison plateau (°)', type: 'number' }, - ], - }, - { - code: 'ventilation-fan', - name: 'Ventilateur process', - description: "Ventilateur d'aspiration ou d'extraction pour ligne céréalière.", - customFields: [ - { name: 'Débit (m³/h)', type: 'number', required: true }, - { - name: 'Type de roue', - type: 'select', - options: ['Axiale', 'Centrifuge', 'Mixte'], - defaultValue: 'Centrifuge', - }, - { name: 'Vitesse nominale (rpm)', type: 'number' }, - ], - }, - { - code: 'control-panel', - name: 'Armoire de contrôle', - description: 'Armoire électrique pilotant un équipement ou une machine.', - customFields: [ - { - name: 'Automate principal', - type: 'select', - required: true, - defaultValue: 'Schneider Modicon M340', - options: ['Schneider Modicon M340', 'Siemens S7-1500', 'Schneider Modicon M221'], - }, - { name: 'Année de mise à jour', type: 'number' }, - { - name: "Indice de protection", - type: 'select', - defaultValue: 'IP55', - options: ['IP55', 'IP65', 'IP54'], - }, - ], - }, - { - code: 'burner-module', - name: 'Module brûleur', - description: 'Module de combustion alimentant un séchoir en air chaud.', - customFields: [ - { name: 'Puissance thermique (kW)', type: 'number', required: true }, - { - name: 'Type de carburant', - type: 'select', - defaultValue: 'Gaz naturel', - options: ['Gaz naturel', 'Biomasse', 'Fioul léger'], - }, - { - name: "Système d'allumage", - type: 'select', - defaultValue: 'Double électrode', - options: ['Double électrode', 'Brûleur pilote', 'Allumeur électronique'], - }, - ], - }, - { - code: 'dryer-column-segment', - name: 'Segment de colonne', - description: 'Module de colonne de séchage empilable.', - customFields: [ - { name: 'Hauteur segment (m)', type: 'number', required: true }, - { - name: 'Zone de séchage', - type: 'select', - options: ['Échauffage', 'Tempe', 'Refroidissement'], - defaultValue: 'Échauffage', - }, - { name: 'Capteurs intégrés', type: 'boolean', defaultValue: 'true' }, - ], - }, - { - code: 'dust-filter', - name: 'Filtre à poussière', - description: 'Filtre à manches ou cyclone pour dépoussiérage.', - customFields: [ - { name: 'Efficacité de filtration (%)', type: 'number', required: true }, - { - name: 'Type de média filtrant', - type: 'select', - options: ['Polyester', 'Fibre de verre', 'Polypropylène'], - defaultValue: 'Polyester', - }, - { name: 'Nombre de cartouches', type: 'number' }, - ], - }, - { - code: 'screw-trough-section', - name: 'Caisson de vis', - description: 'Tronçon de vis sans fin avec palier intermédiaire.', - customFields: [ - { name: 'Longueur (m)', type: 'number', required: true }, - { name: 'Diamètre vis (mm)', type: 'number', required: true }, - { - name: 'Matériau', - type: 'select', - options: ['Acier peint', 'Acier inoxydable', 'Galvanisé'], - defaultValue: 'Acier peint', - }, - ], - }, - { - code: 'screw-inlet-hopper', - name: "Trémie d'alimentation", - description: 'Trémie de reprise amont pour vis sans fin.', - customFields: [ - { name: 'Capacité (L)', type: 'number', required: true }, - { - name: 'Type de grille', - type: 'select', - options: ['Grille lisse', 'Grille anti-gravats', 'Grille magnétique'], - defaultValue: 'Grille anti-gravats', - }, - ], - }, - { - code: 'screw-outlet-chute', - name: 'Goulotte de sortie', - description: 'Sortie aval avec vanne de réglage.', - customFields: [ - { - name: 'Type de vanne', - type: 'select', - options: ['Guillotine', 'Papillon', 'By-pass'], - defaultValue: 'Guillotine', - }, - { name: "Orientation (°)", type: 'number' }, - ], - }, - { - code: 'weigh-load-frame', - name: 'Cadre peseur', - description: 'Structure équipée de capteurs de pesage.', - customFields: [ - { name: 'Capacité nominale (kg)', type: 'number', required: true }, - { name: 'Nombre de capteurs', type: 'number', required: true }, - { - name: 'Protection IP', - type: 'select', - options: ['IP54', 'IP65', 'IP67'], - defaultValue: 'IP65', - }, - ], - }, - { - code: 'weigh-discharge-gate', - name: 'Vanne de vidange', - description: 'Organe de vidange motorisé pour benne peseuse.', - customFields: [ - { - name: "Type d'actionneur", - type: 'select', - options: ['Hydraulique', 'Motoréducteur', 'Pneumatique'], - defaultValue: 'Hydraulique', - }, - { name: "Temps d'ouverture (s)", type: 'number' }, - ], - }, - { - code: 'hydraulic-power-pack', - name: 'Groupe hydraulique', - description: 'Groupe hydraulique alimentant vérins et accessoires.', - customFields: [ - { name: 'Débit nominal (l/min)', type: 'number', required: true }, - { name: 'Pression max (bar)', type: 'number', required: true }, - { - name: 'Type de pompe', - type: 'select', - options: ['Piston axial', 'Palette', 'Engrenages'], - defaultValue: 'Piston axial', - }, - ], - }, - { - code: 'telehandler-boom', - name: 'Flèche télescopique', - description: 'Flèche et berceau de levage pour chariot télescopique.', - customFields: [ - { name: 'Hauteur max (m)', type: 'number', required: true }, - { name: 'Sections télescopiques', type: 'number', required: true }, - { - name: 'Type de guidage', - type: 'select', - options: ['Galets', 'Patins composites'], - defaultValue: 'Galets', - }, - ], - }, - { - code: 'telehandler-cab-module', - name: 'Cabine opérateur', - description: 'Cabine pressurisée avec commandes.', - customFields: [ - { - name: 'Type de cabine', - type: 'select', - options: ['Standard', 'Premium climatisée', 'Haute visibilité'], - defaultValue: 'Premium climatisée', - }, - { name: 'Climatisation', type: 'boolean', defaultValue: 'true' }, - { name: 'Nombre de caméras', type: 'number' }, - ], - }, - { - code: 'telehandler-attachment-carrier', - name: 'Support d’outils', - description: 'Plaque porte-outils et attelage rapide.', - customFields: [ - { - name: "Type d'attache", - type: 'select', - options: ['Fourches FEM', 'Attache Euro', 'Attache Manitou'], - defaultValue: 'Attache Manitou', - }, - { name: 'Capacité nominale (t)', type: 'number', required: true }, - ], - }, -]; - -const pieceTypeDefinitions: PieceTypeDefinition[] = [ - { - code: 'hex-screw', - name: 'Vis hexagonale', - description: 'Visserie pour assemblages mécaniques.', - customFields: [ - { name: 'Diamètre (mm)', type: 'number', required: true }, - { name: 'Longueur (mm)', type: 'number', required: true }, - { - name: 'Classe acier', - type: 'select', - options: ['8.8', '10.9', '12.9'], - defaultValue: '8.8', - }, - ], - }, - { - code: 'lock-washer', - name: 'Rondelle Grower', - description: 'Rondelle anti-desserrage.', - customFields: [ - { name: 'Diamètre (mm)', type: 'number', required: true }, - { - name: 'Finition', - type: 'select', - options: ['Zinguée', 'Inox', 'Noire'], - defaultValue: 'Inox', - }, - ], - }, - { - code: 'flat-gasket', - name: 'Joint plat', - description: 'Joint pour brides et trappes de maintenance.', - customFields: [ - { - name: 'Matière', - type: 'select', - options: ['NBR', 'PTFE', 'Fibre compressée'], - defaultValue: 'NBR', - }, - { name: 'Épaisseur (mm)', type: 'number', required: true }, - ], - }, - { - code: 'drive-belt', - name: 'Courroie', - description: 'Courroie transporteuse ou élévatrice.', - customFields: [ - { name: 'Largeur (mm)', type: 'number', required: true }, - { - name: 'Matériau', - type: 'select', - options: ['Caoutchouc nitrile', 'Polyuréthane', 'PVC'], - defaultValue: 'Caoutchouc nitrile', - }, - ], - }, - { - code: 'roller-bearing', - name: 'Roulement', - description: 'Roulement à semelle ou palier.', - customFields: [ - { - name: 'Série', - type: 'select', - options: ['UCP', 'UCFL', 'UCT'], - defaultValue: 'UCP', - }, - { - name: "Type d'étanchéité", - type: 'select', - options: ['ZZ', '2RS'], - defaultValue: '2RS', - }, - ], - }, - { - code: 'filter-cartridge', - name: 'Cartouche filtrante', - description: 'Cartouche de dépoussiérage.', - customFields: [ - { name: 'Longueur (mm)', type: 'number', required: true }, - { - name: 'Média filtrant', - type: 'select', - options: ['Polyester', 'Fibre de verre', 'Nomex'], - defaultValue: 'Polyester', - }, - ], - }, - { - code: 'speed-sensor', - name: 'Capteur de vitesse', - description: 'Capteur inductif pour surveillance de rotation.', - customFields: [ - { - name: 'Type de sortie', - type: 'select', - options: ['PNP 4-20 mA', 'PNP Tout ou rien'], - defaultValue: 'PNP 4-20 mA', - }, - { name: 'Plage de mesure (rpm)', type: 'number', required: true }, - ], - }, - { - code: 'temperature-probe', - name: 'Sonde de température', - description: 'Sonde PT100 pour surveillance du séchage.', - customFields: [ - { - name: 'Type', - type: 'select', - options: ['PT100 classe A', 'PT100 classe B'], - defaultValue: 'PT100 classe A', - }, - { name: 'Longueur tige (mm)', type: 'number', required: true }, - ], - }, - { - code: 'fuse-cartridge', - name: 'Fusible', - description: 'Fusible cylindrique pour protection électrique.', - customFields: [ - { name: 'Calibre (A)', type: 'number', required: true }, - { - name: 'Type', - type: 'select', - options: ['gG', 'aM'], - defaultValue: 'gG', - }, - ], - }, - { - code: 'load-cell', - name: 'Capteur de pesage', - description: 'Capteur de pesage pour benne peseuse.', - customFields: [ - { name: 'Capacité (kg)', type: 'number', required: true }, - { - name: 'Type de connexion', - type: 'select', - options: ['Câble 4 fils', 'Câble 6 fils'], - defaultValue: 'Câble 6 fils', - }, - ], - }, - { - code: 'lubrication-cartridge', - name: 'Cartouche de graisse', - description: 'Cartouche pour point de graissage automatique.', - customFields: [ - { name: 'Volume (cm³)', type: 'number', required: true }, - { - name: 'Grade de graisse', - type: 'select', - options: ['NLGI 1', 'NLGI 2'], - defaultValue: 'NLGI 2', - }, - ], - }, - { - code: 'hydraulic-hose', - name: 'Flexible hydraulique', - description: 'Flexible haute pression pour circuit hydraulique.', - customFields: [ - { name: 'Pression max (bar)', type: 'number', required: true }, - { - name: 'Type de renfort', - type: 'select', - options: ['2 tresses acier', '4 nappes acier'], - defaultValue: '2 tresses acier', - }, - { name: 'Longueur (mm)', type: 'number', required: true }, - ], - }, -]; - -const pieceModelDefinitions: PieceModelDefinition[] = [ - { - code: 'screw-m12x80', - name: 'Vis M12x80 8.8', - description: 'Vis tête hexagonale pour charpente.', - typeCode: 'hex-screw', - structure: { standard: 'ISO 4014' }, - }, - { - code: 'screw-m10x60', - name: 'Vis M10x60 10.9', - description: 'Vis haute résistance pour convoyeur.', - typeCode: 'hex-screw', - }, - { - code: 'screw-m8x30', - name: 'Vis M8x30 inox', - description: 'Visserie inox pour accessoires.', - typeCode: 'hex-screw', - }, - { - code: 'washer-grower-12', - name: 'Rondelle Grower Ø12', - description: 'Rondelle de sécurité inox.', - typeCode: 'lock-washer', - }, - { - code: 'washer-grower-10', - name: 'Rondelle Grower Ø10', - description: 'Rondelle pour visserie M10.', - typeCode: 'lock-washer', - }, - { - code: 'gasket-ht-200', - name: 'Joint haute température 200°C', - description: 'Joint fibre compressée pour trappe.', - typeCode: 'flat-gasket', - }, - { - code: 'belt-hd-800', - name: 'Courroie 800 mm Heavy Duty', - description: 'Courroie caoutchouc nitrile 800 mm.', - typeCode: 'drive-belt', - }, - { - code: 'belt-hd-650', - name: 'Courroie 650 mm Agro', - description: 'Courroie polyuréthane 650 mm.', - typeCode: 'drive-belt', - }, - { - code: 'bearing-ucp210', - name: 'Palier UCP210', - description: 'Roulement semelle fonte.', - typeCode: 'roller-bearing', - }, - { - code: 'bearing-ucfl207', - name: 'Palier UCFL207', - description: 'Roulement bride deux trous.', - typeCode: 'roller-bearing', - }, - { - code: 'filter-dust-610', - name: 'Cartouche filtre 610 mm', - description: 'Cartouche polyester 610 mm.', - typeCode: 'filter-cartridge', - }, - { - code: 'filter-dust-480', - name: 'Cartouche filtre 480 mm', - description: 'Cartouche polyester 480 mm.', - typeCode: 'filter-cartridge', - }, - { - code: 'sensor-speed-m12', - name: 'Capteur vitesse M12', - description: 'Capteur inductif M12 PNP 4-20 mA.', - typeCode: 'speed-sensor', - }, - { - code: 'sensor-speed-m18', - name: 'Capteur vitesse M18', - description: 'Capteur inductif M18 PNP.', - typeCode: 'speed-sensor', - }, - { - code: 'temp-probe-pt100', - name: 'Sonde PT100 300 mm', - description: 'Sonde PT100 classe A tige 300 mm.', - typeCode: 'temperature-probe', - }, - { - code: 'temp-probe-pt100-short', - name: 'Sonde PT100 200 mm', - description: 'Sonde PT100 compacte.', - typeCode: 'temperature-probe', - }, - { - code: 'fuse-gg-25a', - name: 'Fusible gG 25A', - description: 'Fusible protection puissance 25A.', - typeCode: 'fuse-cartridge', - }, - { - code: 'fuse-gg-40a', - name: 'Fusible gG 40A', - description: 'Fusible protection 40A.', - typeCode: 'fuse-cartridge', - }, - { - code: 'load-cell-5t', - name: 'Capteur pesage 5T', - description: 'Capteur compression 5 tonnes.', - typeCode: 'load-cell', - }, - { - code: 'grease-cartridge-400', - name: 'Cartouche graisse 400g', - description: 'Cartouche graisse NLGI2 400g.', - typeCode: 'lubrication-cartridge', - }, - { - code: 'hydraulic-hose-2w', - name: 'Flexible 2 tresses 420 bar', - description: 'Flexible 2 tresses acier 420 bar.', - typeCode: 'hydraulic-hose', - }, -]; - -const componentModelDefinitions: ComponentModelDefinition[] = [ - { - code: 'motor-drive-75', - name: 'Moteur 75 kW IE3', - description: 'Moteur IE3 75 kW IP55.', - typeCode: 'motor-drive', - structure: { - recommendedCustomFields: { - 'Puissance nominale (kW)': '75', - 'Classe énergétique': 'IE3', - "Indice de protection": 'IP55', - }, - }, - }, - { - code: 'motor-drive-55', - name: 'Moteur 55 kW IE3', - description: 'Motorisation ligne aval.', - typeCode: 'motor-drive', - structure: { - recommendedCustomFields: { - 'Puissance nominale (kW)': '55', - 'Classe énergétique': 'IE3', - "Indice de protection": 'IP55', - }, - pieceTemplates: [ - { - typeCode: 'roller-bearing', - modelCode: 'bearing-ucfl207', - quantity: 2, - usage: 'Support pattes moteur', - }, - { - typeCode: 'hex-screw', - modelCode: 'screw-m10x60', - quantity: 6, - usage: 'Fixations supérieures', - }, - ], - maintenanceNotes: 'Contrôle vibrations mensuel, resserrage annuel.', - }, - }, - { - code: 'motor-drive-45', - name: 'Moteur 45 kW IE3', - description: 'Motorisation convoyeur principal.', - typeCode: 'motor-drive', - structure: { - recommendedCustomFields: { - 'Puissance nominale (kW)': '45', - 'Classe énergétique': 'IE3', - "Indice de protection": 'IP55', - }, - pieceTemplates: [ - { - typeCode: 'roller-bearing', - modelCode: 'bearing-ucfl207', - quantity: 2, - usage: 'Palier de sortie', - }, - ], - subComponentTemplates: [ - { - typeCode: 'gearbox-assembly', - suggestedModelCodes: ['gearbox-sew'], - notes: 'Prévoir accouplement conique.', - }, - ], - }, - }, - { - code: 'motor-drive-37', - name: 'Moteur 37 kW IE3', - description: 'Motorisation convoyeur secondaire.', - typeCode: 'motor-drive', - structure: { - recommendedCustomFields: { - 'Puissance nominale (kW)': '37', - 'Classe énergétique': 'IE3', - "Indice de protection": 'IP55', - }, - pieceTemplates: [ - { - typeCode: 'roller-bearing', - modelCode: 'bearing-ucp210', - quantity: 2, - usage: 'Roulements moteur', - }, - ], - }, - }, - { - code: 'motor-drive-18', - name: 'Moteur 18.5 kW IE3', - description: 'Motorisation vis sans fin.', - typeCode: 'motor-drive', - structure: { - recommendedCustomFields: { - 'Puissance nominale (kW)': '18.5', - 'Classe énergétique': 'IE3', - "Indice de protection": 'IP55', - }, - pieceTemplates: [ - { - typeCode: 'hex-screw', - modelCode: 'screw-m10x60', - quantity: 4, - usage: 'Fixation semelle', - }, - ], - subComponentTemplates: [ - { - typeCode: 'gearbox-assembly', - suggestedModelCodes: ['gearbox-sew'], - notes: 'Couple conique pour vis sans fin.', - }, - ], - }, - }, - { - code: 'motor-drive-15', - name: 'Moteur 15 kW IE3', - description: 'Motorisation vis compact.', - typeCode: 'motor-drive', - structure: { - recommendedCustomFields: { - 'Puissance nominale (kW)': '15', - 'Classe énergétique': 'IE3', - "Indice de protection": 'IP55', - }, - pieceTemplates: [ - { - typeCode: 'hex-screw', - modelCode: 'screw-m8x30', - quantity: 6, - usage: 'Fixation petits supports', - }, - ], - }, - }, - { - code: 'motor-drive-110', - name: 'Moteur 110 kW IE3', - description: 'Motorisation séchoir.', - typeCode: 'motor-drive', - structure: { - recommendedCustomFields: { - 'Puissance nominale (kW)': '110', - 'Classe énergétique': 'IE3', - "Indice de protection": 'IP55', - }, - pieceTemplates: [ - { - typeCode: 'roller-bearing', - modelCode: 'bearing-ucp210', - quantity: 2, - usage: 'Roulements renforts', - }, - { - typeCode: 'hex-screw', - modelCode: 'screw-m12x80', - quantity: 12, - usage: 'Ancrage châssis', - }, - ], - maintenanceNotes: 'Contrôle isolation bobinage semestriel.', - }, - }, - { - code: 'gearbox-flender', - name: 'Réducteur Flender 3200 Nm', - description: 'Réducteur à couple élevé.', - typeCode: 'gearbox-assembly', - structure: { - recommendedCustomFields: { - 'Rapport de réduction': '1:28', - 'Couple nominal (Nm)': '3200', - 'Type de montage': 'À bride', - }, - pieceTemplates: [ - { - typeCode: 'hex-screw', - modelCode: 'screw-m12x80', - quantity: 12, - usage: 'Fixation bride réducteur', - }, - { - typeCode: 'lock-washer', - modelCode: 'washer-grower-12', - quantity: 12, - usage: 'Sécurisation boulonnerie', - }, - ], - subComponentTemplates: [ - { - typeCode: 'motor-drive', - suggestedModelCodes: ['motor-drive-75', 'motor-drive-55'], - notes: 'Association moteur IE3 selon besoin.', - }, - ], - }, - }, - { - code: 'gearbox-bonfiglioli', - name: 'Réducteur Bonfiglioli TA', - description: 'Réducteur montage sur arbre.', - typeCode: 'gearbox-assembly', - structure: { - recommendedCustomFields: { - 'Rapport de réduction': '1:24', - 'Couple nominal (Nm)': '2100', - 'Type de montage': 'Sur arbre', - }, - pieceTemplates: [ - { - typeCode: 'hex-screw', - modelCode: 'screw-m10x60', - quantity: 8, - usage: 'Fixations bras couple', - }, - { - typeCode: 'lock-washer', - modelCode: 'washer-grower-10', - quantity: 8, - usage: 'Sécurisation fixations', - }, - ], - }, - }, - { - code: 'gearbox-sew', - name: 'Réducteur SEW K', - description: 'Réducteur coaxial pour vis.', - typeCode: 'gearbox-assembly', - structure: { - recommendedCustomFields: { - 'Rapport de réduction': '1:18', - 'Couple nominal (Nm)': '1800', - 'Type de montage': 'À bride', - }, - pieceTemplates: [ - { - typeCode: 'lock-washer', - modelCode: 'washer-grower-12', - quantity: 10, - usage: 'Contre-écrous', - }, - ], - subComponentTemplates: [ - { - typeCode: 'motor-drive', - suggestedModelCodes: ['motor-drive-18', 'motor-drive-15'], - notes: 'Motorisation vis standard.', - }, - ], - }, - }, - { - code: 'bucket-head-120', - name: 'Tête élévateur 800 mm', - description: 'Tête renforcée 800 mm.', - typeCode: 'bucket-head-section', - structure: { - recommendedCustomFields: { - 'Largeur tambour (mm)': '820', - 'Type de revêtement': 'Caoutchouc rainuré', - 'Nombre de trappes': '3', - }, - pieceTemplates: [ - { - typeCode: 'hex-screw', - modelCode: 'screw-m12x80', - quantity: 24, - usage: 'Assemblage flasques', - }, - { - typeCode: 'lock-washer', - modelCode: 'washer-grower-12', - quantity: 24, - usage: 'Sécurisation visserie', - }, - { - typeCode: 'speed-sensor', - modelCode: 'sensor-speed-m12', - quantity: 1, - usage: 'Contrôle vitesse tambour', - }, - ], - subComponentTemplates: [ - { - typeCode: 'motor-drive', - suggestedModelCodes: ['motor-drive-75'], - notes: 'Motorisation tête élévateur', - }, - { - typeCode: 'gearbox-assembly', - suggestedModelCodes: ['gearbox-flender'], - notes: 'Réducteur couple élevé', - }, - ], - }, - }, - { - code: 'bucket-head-95', - name: 'Tête élévateur 650 mm', - description: 'Tête compacte 650 mm.', - typeCode: 'bucket-head-section', - structure: { - recommendedCustomFields: { - 'Largeur tambour (mm)': '660', - 'Type de revêtement': 'Polyuréthane', - 'Nombre de trappes': '2', - }, - pieceTemplates: [ - { - typeCode: 'hex-screw', - modelCode: 'screw-m10x60', - quantity: 18, - usage: 'Assemblage coiffe', - }, - { - typeCode: 'lock-washer', - modelCode: 'washer-grower-10', - quantity: 18, - usage: 'Sécurité assemblage', - }, - ], - subComponentTemplates: [ - { - typeCode: 'motor-drive', - suggestedModelCodes: ['motor-drive-55'], - notes: 'Motorisation compacte', - }, - { - typeCode: 'gearbox-assembly', - suggestedModelCodes: ['gearbox-bonfiglioli'], - notes: 'Montage sur arbre', - }, - ], - }, - }, - { - code: 'bucket-boot-heavy', - name: 'Pied élévateur renforcé', - description: 'Pied avec tension à vis.', - typeCode: 'bucket-boot-section', - structure: { - recommendedCustomFields: { - 'Capacité trémie (L)': '480', - 'Mode de tension': 'Vis manuelle', - 'Système de nettoyage': 'Grattoirs', - }, - pieceTemplates: [ - { - typeCode: 'flat-gasket', - modelCode: 'gasket-ht-200', - quantity: 2, - usage: 'Etanchéité trappes', - }, - { - typeCode: 'hex-screw', - modelCode: 'screw-m12x80', - quantity: 16, - usage: 'Assemblage flasques', - }, - ], - subComponentTemplates: [ - { - typeCode: 'motor-drive', - suggestedModelCodes: ['motor-drive-18'], - notes: 'Motorisation vis de nettoyage', - }, - ], - }, - }, - { - code: 'bucket-boot-compact', - name: 'Pied élévateur compact', - description: 'Pied compact avec tension hydraulique.', - typeCode: 'bucket-boot-section', - structure: { - recommendedCustomFields: { - 'Capacité trémie (L)': '320', - 'Mode de tension': 'Vérin hydraulique', - 'Système de nettoyage': 'Trappe déportée', - }, - pieceTemplates: [ - { - typeCode: 'flat-gasket', - modelCode: 'gasket-ht-200', - quantity: 1, - usage: 'Joint inspection', - }, - ], - }, - }, - { - code: 'bucket-leg-3m', - name: 'Tronçon 3 m', - description: 'Gaine 3 mètres renforcée.', - typeCode: 'bucket-leg-section', - structure: { - recommendedCustomFields: { - 'Hauteur section (m)': '3', - 'Type de gaine': 'Boulonnée', - }, - pieceTemplates: [ - { - typeCode: 'hex-screw', - modelCode: 'screw-m12x80', - quantity: 32, - usage: 'Assemblage brides', - }, - ], - }, - }, - { - code: 'bucket-leg-2-5m', - name: 'Tronçon 2,5 m', - description: 'Gaine 2,5 mètres compacte.', - typeCode: 'bucket-leg-section', - structure: { - recommendedCustomFields: { - 'Hauteur section (m)': '2.5', - 'Type de gaine': 'Soudée', - }, - pieceTemplates: [ - { - typeCode: 'hex-screw', - modelCode: 'screw-m10x60', - quantity: 24, - usage: 'Assemblage brides', - }, - ], - }, - }, - { - code: 'belt-drive-800', - name: "Station entraînement 800", - description: 'Station tête 800 mm.', - typeCode: 'belt-drive-station', - structure: { - recommendedCustomFields: { - 'Diamètre tambour (mm)': '630', - 'Type de racleur': 'Polyuréthane', - }, - pieceTemplates: [ - { - typeCode: 'roller-bearing', - modelCode: 'bearing-ucp210', - quantity: 2, - usage: 'Paliers tambour', - }, - { - typeCode: 'drive-belt', - modelCode: 'belt-hd-800', - quantity: 1, - usage: 'Bande principale', - }, - ], - subComponentTemplates: [ - { - typeCode: 'motor-drive', - suggestedModelCodes: ['motor-drive-45'], - notes: 'Motorisation convoyeur', - }, - { - typeCode: 'gearbox-assembly', - suggestedModelCodes: ['gearbox-bonfiglioli'], - notes: 'Réducteur montage sur arbre', - }, - ], - }, - }, - { - code: 'belt-drive-650', - name: "Station entraînement 650", - description: 'Station tête 650 mm.', - typeCode: 'belt-drive-station', - structure: { - recommendedCustomFields: { - 'Diamètre tambour (mm)': '520', - 'Type de racleur': 'Caoutchouc', - }, - pieceTemplates: [ - { - typeCode: 'roller-bearing', - modelCode: 'bearing-ucfl207', - quantity: 2, - usage: 'Paliers excentriques', - }, - ], - subComponentTemplates: [ - { - typeCode: 'motor-drive', - suggestedModelCodes: ['motor-drive-37'], - notes: 'Motorisation secondaire', - }, - ], - }, - }, - { - code: 'belt-tail-800', - name: 'Station retour 800', - description: 'Retour 800 mm avec racleur.', - typeCode: 'belt-tail-station', - structure: { - recommendedCustomFields: { - 'Type de racleur retour': 'Racleur bande mousse', - 'Système tension': 'Vis double', - }, - pieceTemplates: [ - { - typeCode: 'roller-bearing', - modelCode: 'bearing-ucp210', - quantity: 2, - usage: 'Paliers tambour retour', - }, - { - typeCode: 'hex-screw', - modelCode: 'screw-m12x80', - quantity: 12, - usage: 'Fixation réglages', - }, - ], - }, - }, - { - code: 'belt-tail-650', - name: 'Station retour 650', - description: 'Retour 650 mm compact.', - typeCode: 'belt-tail-station', - structure: { - recommendedCustomFields: { - 'Type de racleur retour': 'Racleur brosse', - 'Système tension': 'Poids', - }, - pieceTemplates: [ - { - typeCode: 'roller-bearing', - modelCode: 'bearing-ucfl207', - quantity: 2, - usage: 'Paliers retour', - }, - ], - }, - }, - { - code: 'belt-frame-18m', - name: 'Ossature 18 m', - description: 'Châssis convoyeur 18 m.', - typeCode: 'belt-support-frame', - structure: { - recommendedCustomFields: { - 'Longueur module (m)': '6', - 'Nombre de rouleaux': '45', - }, - pieceTemplates: [ - { - typeCode: 'hex-screw', - modelCode: 'screw-m12x80', - quantity: 60, - usage: 'Assemblage modules', - }, - ], - }, - }, - { - code: 'belt-frame-25m', - name: 'Ossature 25 m', - description: 'Châssis convoyeur 25 m.', - typeCode: 'belt-support-frame', - structure: { - recommendedCustomFields: { - 'Longueur module (m)': '8.3', - 'Nombre de rouleaux': '68', - }, - pieceTemplates: [ - { - typeCode: 'hex-screw', - modelCode: 'screw-m12x80', - quantity: 84, - usage: 'Assemblage modules', - }, - ], - }, - }, - { - code: 'gravity-deck-120', - name: 'Plateau densimétrique 120', - description: 'Plateau haute capacité.', - typeCode: 'gravity-vibration-deck', - structure: { - recommendedCustomFields: { - 'Inclinaison plateau (°)': '6', - 'Tamis maille (µm)': '650', - 'Amplitude vibration (mm)': '4.5', - }, - pieceTemplates: [ - { - typeCode: 'speed-sensor', - modelCode: 'sensor-speed-m18', - quantity: 2, - usage: 'Suivi vibrations', - }, - { - typeCode: 'filter-cartridge', - modelCode: 'filter-dust-610', - quantity: 1, - usage: 'Aspiration plateau', - }, - ], - subComponentTemplates: [ - { - typeCode: 'ventilation-fan', - suggestedModelCodes: ['fan-process-45'], - notes: 'Aspiration principale', - }, - ], - }, - }, - { - code: 'gravity-deck-80', - name: 'Plateau densimétrique 80', - description: 'Plateau compact.', - typeCode: 'gravity-vibration-deck', - structure: { - recommendedCustomFields: { - 'Inclinaison plateau (°)': '5', - 'Tamis maille (µm)': '450', - 'Amplitude vibration (mm)': '3.2', - }, - pieceTemplates: [ - { - typeCode: 'speed-sensor', - modelCode: 'sensor-speed-m12', - quantity: 1, - usage: 'Capteur de vibration', - }, - ], - }, - }, - { - code: 'fan-process-45', - name: 'Ventilateur 45 kW', - description: 'Ventilateur centrifuge 45 kW.', - typeCode: 'ventilation-fan', - structure: { - recommendedCustomFields: { - 'Débit (m³/h)': '48000', - 'Pression disponible (Pa)': '3200', - 'Sens de rotation': 'Horaire', - }, - pieceTemplates: [ - { - typeCode: 'roller-bearing', - modelCode: 'bearing-ucp210', - quantity: 2, - usage: 'Roulements arbre ventilateur', - }, - { - typeCode: 'temperature-probe', - modelCode: 'temp-probe-pt100', - quantity: 1, - usage: 'Surveillance chauffe palier', - }, - ], - subComponentTemplates: [ - { - typeCode: 'motor-drive', - suggestedModelCodes: ['motor-drive-110'], - notes: 'Entraînement direct haute puissance', - }, - ], - }, - }, - { - code: 'fan-process-30', - name: 'Ventilateur 30 kW', - description: 'Ventilateur centrifuge 30 kW.', - typeCode: 'ventilation-fan', - structure: { - recommendedCustomFields: { - 'Débit (m³/h)': '28000', - 'Pression disponible (Pa)': '2200', - 'Sens de rotation': 'Antihoraire', - }, - pieceTemplates: [ - { - typeCode: 'roller-bearing', - modelCode: 'bearing-ucfl207', - quantity: 2, - usage: 'Roulements', - }, - { - typeCode: 'temperature-probe', - modelCode: 'temp-probe-pt100-short', - quantity: 1, - usage: 'Sonde palier', - }, - ], - }, - }, - { - code: 'control-panel-m340', - name: 'Armoire Schneider M340', - description: 'Armoire Schneider Electric.', - typeCode: 'control-panel', - structure: { - recommendedCustomFields: { - 'Automate principal': 'Schneider Modicon M340', - 'Indice de protection': 'IP55', - 'Capacité IO': '48', - }, - pieceTemplates: [ - { - typeCode: 'fuse-cartridge', - modelCode: 'fuse-gg-25a', - quantity: 3, - usage: 'Protection générale', - }, - { - typeCode: 'temperature-probe', - modelCode: 'temp-probe-pt100-short', - quantity: 1, - usage: 'Sonde ambiance armoire', - }, - ], - subComponentTemplates: [ - { - typeCode: 'control-panel', - suggestedModelCodes: ['control-panel-s7'], - notes: 'Extension communication', - }, - ], - }, - }, - { - code: 'control-panel-s7', - name: 'Armoire Siemens S7', - description: 'Armoire Siemens S7-1500.', - typeCode: 'control-panel', - structure: { - recommendedCustomFields: { - 'Automate principal': 'Siemens S7-1500', - 'Indice de protection': 'IP54', - 'Capacité IO': '64', - }, - pieceTemplates: [ - { - typeCode: 'fuse-cartridge', - modelCode: 'fuse-gg-40a', - quantity: 3, - usage: 'Protection circuits puissance', - }, - { - typeCode: 'temperature-probe', - modelCode: 'temp-probe-pt100', - quantity: 1, - usage: 'Sonde coffret', - }, - ], - }, - }, - { - code: 'burner-module-3mw', - name: 'Brûleur gaz 3 MW', - description: 'Brûleur gaz modulant.', - typeCode: 'burner-module', - structure: { - recommendedCustomFields: { - 'Puissance thermique (MW)': '3', - 'Type de combustible': 'Gaz naturel', - 'Taux O2 résiduel (%)': '4', - }, - pieceTemplates: [ - { - typeCode: 'temperature-probe', - modelCode: 'temp-probe-pt100', - quantity: 2, - usage: 'Surveillance foyer', - }, - { - typeCode: 'hydraulic-hose', - modelCode: 'hydraulic-hose-2w', - quantity: 1, - usage: 'Alimentation vérin clapet', - }, - ], - subComponentTemplates: [ - { - typeCode: 'dust-filter', - suggestedModelCodes: ['dust-filter-cyclone'], - notes: 'Aspiration fumées', - }, - ], - }, - }, - { - code: 'burner-module-2mw', - name: 'Brûleur biomasse 2 MW', - description: 'Brûleur biomasse compact.', - typeCode: 'burner-module', - structure: { - recommendedCustomFields: { - 'Puissance thermique (MW)': '2', - 'Type de combustible': 'Biomasse', - 'Taux O2 résiduel (%)': '5', - }, - pieceTemplates: [ - { - typeCode: 'temperature-probe', - modelCode: 'temp-probe-pt100-short', - quantity: 2, - usage: 'Surveillance foyer', - }, - { - typeCode: 'flat-gasket', - modelCode: 'gasket-ht-200', - quantity: 4, - usage: 'Trappes de visite', - }, - ], - }, - }, - { - code: 'dryer-column-2m', - name: 'Segment colonne 2 m', - description: 'Segment de colonne zone chaude.', - typeCode: 'dryer-column-segment', - structure: { - recommendedCustomFields: { - 'Hauteur segment (m)': '2', - 'Zone procédé': 'Chauffage', - 'Isolation (mm)': '80', - }, - pieceTemplates: [ - { - typeCode: 'hex-screw', - modelCode: 'screw-m12x80', - quantity: 40, - usage: 'Assemblage panneaux', - }, - { - typeCode: 'flat-gasket', - modelCode: 'gasket-ht-200', - quantity: 6, - usage: 'Joints inspections', - }, - ], - subComponentTemplates: [ - { - typeCode: 'burner-module', - suggestedModelCodes: ['burner-module-3mw'], - notes: 'Brûleur associé zone chaude', - }, - ], - }, - }, - { - code: 'dryer-column-1-5m', - name: 'Segment colonne 1,5 m', - description: 'Segment zone refroidissement.', - typeCode: 'dryer-column-segment', - structure: { - recommendedCustomFields: { - 'Hauteur segment (m)': '1.5', - 'Zone procédé': 'Refroidissement', - 'Isolation (mm)': '60', - }, - pieceTemplates: [ - { - typeCode: 'hex-screw', - modelCode: 'screw-m10x60', - quantity: 32, - usage: 'Assemblage panneaux', - }, - { - typeCode: 'filter-cartridge', - modelCode: 'filter-dust-480', - quantity: 1, - usage: 'Filtration air', - }, - ], - }, - }, - { - code: 'dust-filter-cyclone', - name: 'Filtre cyclone 610', - description: 'Filtre cyclone 6 cartouches.', - typeCode: 'dust-filter', - structure: { - recommendedCustomFields: { - 'Nombre de cartouches': '6', - 'Surface filtration (m²)': '48', - 'Mode nettoyage': 'Pulse jet', - }, - pieceTemplates: [ - { - typeCode: 'filter-cartridge', - modelCode: 'filter-dust-610', - quantity: 6, - usage: 'Cartouches filtrantes', - }, - { - typeCode: 'temperature-probe', - modelCode: 'temp-probe-pt100', - quantity: 1, - usage: 'Sonde température air', - }, - ], - subComponentTemplates: [ - { - typeCode: 'ventilation-fan', - suggestedModelCodes: ['fan-process-45'], - notes: 'Ventilateur extraction', - }, - ], - }, - }, - { - code: 'dust-filter-compact', - name: 'Filtre compact 480', - description: 'Filtre compact 4 cartouches.', - typeCode: 'dust-filter', - structure: { - recommendedCustomFields: { - 'Nombre de cartouches': '4', - 'Surface filtration (m²)': '28', - 'Mode nettoyage': 'Vibration', - }, - pieceTemplates: [ - { - typeCode: 'filter-cartridge', - modelCode: 'filter-dust-480', - quantity: 4, - usage: 'Cartouches filtrantes', - }, - ], - }, - }, - { - code: 'screw-trough-200', - name: 'Caisson vis 200', - description: 'Caisson pour vis 200 mm.', - typeCode: 'screw-trough-section', - structure: { - recommendedCustomFields: { - 'Diamètre vis (mm)': '200', - 'Longueur module (m)': '2', - 'Finition intérieure': 'Galvanisée', - }, - pieceTemplates: [ - { - typeCode: 'hex-screw', - modelCode: 'screw-m10x60', - quantity: 28, - usage: 'Assemblage couvercles', - }, - { - typeCode: 'lock-washer', - modelCode: 'washer-grower-10', - quantity: 28, - usage: 'Maintien visserie', - }, - ], - subComponentTemplates: [ - { - typeCode: 'motor-drive', - suggestedModelCodes: ['motor-drive-18'], - notes: 'Motorisation vis 200', - }, - { - typeCode: 'gearbox-assembly', - suggestedModelCodes: ['gearbox-sew'], - notes: 'Réducteur vis 200', - }, - ], - }, - }, - { - code: 'screw-trough-160', - name: 'Caisson vis 160', - description: 'Caisson pour vis 160 mm.', - typeCode: 'screw-trough-section', - structure: { - recommendedCustomFields: { - 'Diamètre vis (mm)': '160', - 'Longueur module (m)': '1.6', - 'Finition intérieure': 'Peinte', - }, - pieceTemplates: [ - { - typeCode: 'hex-screw', - modelCode: 'screw-m8x30', - quantity: 24, - usage: 'Assemblage couvercles', - }, - ], - subComponentTemplates: [ - { - typeCode: 'motor-drive', - suggestedModelCodes: ['motor-drive-15'], - notes: 'Motorisation vis 160', - }, - ], - }, - }, - { - code: 'screw-inlet-200', - name: 'Trémie alimentation 200', - description: 'Trémie 200 l anti-gravats.', - typeCode: 'screw-inlet-hopper', - structure: { - recommendedCustomFields: { - 'Capacité (L)': '200', - 'Type grille': 'Anti-gravats', - 'Présence capteur niveau': 'Oui', - }, - pieceTemplates: [ - { - typeCode: 'speed-sensor', - modelCode: 'sensor-speed-m12', - quantity: 1, - usage: 'Capteur niveau vibrant', - }, - { - typeCode: 'hex-screw', - modelCode: 'screw-m8x30', - quantity: 12, - usage: 'Fixation grille', - }, - ], - }, - }, - { - code: 'screw-inlet-160', - name: 'Trémie alimentation 160', - description: 'Trémie 160 l compacte.', - typeCode: 'screw-inlet-hopper', - structure: { - recommendedCustomFields: { - 'Capacité (L)': '160', - 'Type grille': 'Maille fine', - 'Présence capteur niveau': 'Non', - }, - pieceTemplates: [ - { - typeCode: 'hex-screw', - modelCode: 'screw-m8x30', - quantity: 8, - usage: 'Fixation couvercle', - }, - ], - }, - }, - { - code: 'screw-outlet-vanne', - name: 'Goulotte vanne guillotine', - description: 'Goulotte sortie vanne guillotine.', - typeCode: 'screw-outlet-chute', - structure: { - recommendedCustomFields: { - 'Type de vanne': 'Guillotine', - 'Commande': 'Manuelle', - 'Largeur (mm)': '220', - }, - pieceTemplates: [ - { - typeCode: 'flat-gasket', - modelCode: 'gasket-ht-200', - quantity: 2, - usage: "Joint d'accouplement", - }, - { - typeCode: 'hex-screw', - modelCode: 'screw-m10x60', - quantity: 10, - usage: 'Fixation bride', - }, - ], - }, - }, - { - code: 'screw-outlet-flap', - name: 'Goulotte vanne clapet', - description: 'Goulotte sortie vanne clapet.', - typeCode: 'screw-outlet-chute', - structure: { - recommendedCustomFields: { - 'Type de vanne': 'Clapet', - 'Commande': 'Pneumatique', - 'Largeur (mm)': '180', - }, - pieceTemplates: [ - { - typeCode: 'hydraulic-hose', - modelCode: 'hydraulic-hose-2w', - quantity: 1, - usage: 'Raccordement vérin', - }, - ], - }, - }, - { - code: 'weigh-frame-5t', - name: 'Cadre peseur 5T', - description: 'Cadre peseur 5 tonnes.', - typeCode: 'weigh-load-frame', - structure: { - recommendedCustomFields: { - 'Capacité pesée (kg)': '5000', - 'Matière': 'Acier galvanisé', - 'Nombre de cellules': '4', - }, - pieceTemplates: [ - { - typeCode: 'load-cell', - modelCode: 'load-cell-5t', - quantity: 4, - usage: 'Cellules de pesée', - }, - { - typeCode: 'hex-screw', - modelCode: 'screw-m12x80', - quantity: 16, - usage: 'Fixation platines', - }, - ], - }, - }, - { - code: 'weigh-frame-3t', - name: 'Cadre peseur 3T', - description: 'Cadre peseur 3 tonnes.', - typeCode: 'weigh-load-frame', - structure: { - recommendedCustomFields: { - 'Capacité pesée (kg)': '3000', - 'Matière': 'Acier peint', - 'Nombre de cellules': '4', - }, - pieceTemplates: [ - { - typeCode: 'load-cell', - modelCode: 'load-cell-5t', - quantity: 4, - usage: 'Cellules de pesée', - }, - ], - }, - }, - { - code: 'weigh-gate-hydraulic', - name: 'Vanne hydraulique', - description: 'Vanne hydraulique ouverture rapide.', - typeCode: 'weigh-discharge-gate', - structure: { - recommendedCustomFields: { - 'Type d’actionnement': 'Hydraulique', - 'Temps ouverture (s)': '2.5', - 'Pression service (bar)': '180', - }, - pieceTemplates: [ - { - typeCode: 'hydraulic-hose', - modelCode: 'hydraulic-hose-2w', - quantity: 2, - usage: 'Lignes de puissance', - }, - ], - }, - }, - { - code: 'weigh-gate-screw', - name: 'Vanne vis motorisée', - description: 'Vanne motoréducteur vis.', - typeCode: 'weigh-discharge-gate', - structure: { - recommendedCustomFields: { - 'Type d’actionnement': 'Motoréducteur vis', - 'Temps ouverture (s)': '6', - 'Couple (Nm)': '450', - }, - pieceTemplates: [ - { - typeCode: 'motor-drive', - modelCode: 'motor-drive-15', - quantity: 1, - usage: 'Motorisation vanne', - }, - ], - subComponentTemplates: [ - { - typeCode: 'gearbox-assembly', - suggestedModelCodes: ['gearbox-sew'], - notes: 'Réducteur vanne', - }, - ], - }, - }, - { - code: 'hydraulic-pack-tele', - name: 'Groupe hydraulique 140 l/min', - description: 'Groupe hydraulique Manitou.', - typeCode: 'hydraulic-power-pack', - structure: { - recommendedCustomFields: { - 'Débit nominal (l/min)': '140', - 'Pression nominale (bar)': '250', - 'Nombre de distributeurs': '3', - }, - pieceTemplates: [ - { - typeCode: 'hydraulic-hose', - modelCode: 'hydraulic-hose-2w', - quantity: 4, - usage: 'Lignes auxiliaires', - }, - { - typeCode: 'lubrication-cartridge', - modelCode: 'grease-cartridge-400', - quantity: 2, - usage: 'Graissage articulations', - }, - ], - }, - }, - { - code: 'tele-boom-8m', - name: 'Flèche télescopique 8 m', - description: 'Flèche 4 sections 8 m.', - typeCode: 'telehandler-boom', - structure: { - recommendedCustomFields: { - 'Hauteur max (m)': '8', - 'Nombre de sections': '4', - 'Traitement anticorrosion': 'Peinture époxy', - }, - pieceTemplates: [ - { - typeCode: 'hydraulic-hose', - modelCode: 'hydraulic-hose-2w', - quantity: 3, - usage: 'Vérins télescopiques', - }, - ], - subComponentTemplates: [ - { - typeCode: 'hydraulic-power-pack', - suggestedModelCodes: ['hydraulic-pack-tele'], - notes: 'Alimentation vérins', - }, - ], - }, - }, - { - code: 'tele-cab-premium', - name: 'Cabine premium climatisée', - description: 'Cabine Manitou climatisée.', - typeCode: 'telehandler-cab-module', - structure: { - recommendedCustomFields: { - 'Climatisation': 'Oui', - 'Suspension cabine': 'Hydraulique', - 'Siège chauffant': 'Oui', - }, - pieceTemplates: [ - { - typeCode: 'temperature-probe', - modelCode: 'temp-probe-pt100-short', - quantity: 1, - usage: 'Sonde confort cabine', - }, - ], - }, - }, - { - code: 'tele-carrier-quick', - name: 'Attache rapide universelle', - description: 'Support d’outils Manitou.', - typeCode: 'telehandler-attachment-carrier', - structure: { - recommendedCustomFields: { - 'Type attache': 'Euro', - 'Capacité (kg)': '3500', - 'Verrouillage hydraulique': 'Oui', - }, - pieceTemplates: [ - { - typeCode: 'hex-screw', - modelCode: 'screw-m12x80', - quantity: 10, - usage: 'Fixation crochets', - }, - { - typeCode: 'hydraulic-hose', - modelCode: 'hydraulic-hose-2w', - quantity: 1, - usage: 'Commande verrouillage', - }, - ], - }, - }, -]; - -const constructeurDefinitions: ConstructeurDefinition[] = [ - { - key: 'buhler', - name: 'Bühler Grain Systems', - email: 'contact@buhlergroup.com', - phone: '+41 71 955 11 11', - }, - { - key: 'agritech', - name: 'Agritech Elevators', - email: 'info@agritech.fr', - phone: '+33 4 78 12 34 56', - }, - { - key: 'valmont', - name: 'Valmont Handling', - email: 'sales@valmont.fr', - phone: '+33 3 88 77 41 20', - }, - { - key: 'agridry', - name: 'Agridry Systems', - email: 'support@agridry.eu', - phone: '+33 2 41 52 78 00', - }, - { - key: 'agrifan', - name: 'AgriFan Ventilation', - email: 'contact@agrifan.fr', - phone: '+33 2 44 55 12 32', - }, - { - key: 'manitou', - name: 'Manitou Group', - email: 'support@manitou-group.com', - phone: '+33 2 40 09 10 11', - }, - { - key: 'sew', - name: 'SEW-Eurodrive', - email: 'info@sew-eurodrive.fr', - phone: '+33 4 72 24 60 00', - }, - { - key: 'flender', - name: 'Flender GmbH', - email: 'contact@flender.com', - phone: '+49 203 998 0', - }, - { - key: 'bonfiglioli', - name: 'Bonfiglioli Riduttori', - email: 'sales@bonfiglioli.com', - phone: '+39 051 647 3111', - }, - { - key: 'poclain', - name: 'Poclain Hydraulics', - email: 'support@poclain-hydraulics.com', - phone: '+33 3 44 31 74 00', - }, - { - key: 'ifm', - name: 'IFM Electronic', - email: 'contact@ifm.com', - phone: '+33 1 69 11 37 00', - }, - { - key: 'skf', - name: 'SKF France', - email: 'support@skf.com', - phone: '+33 4 37 24 64 00', - }, -]; - -const typeMachineDefinitions: TypeMachineDefinition[] = [ - { - code: 'bucket-elevator', - name: 'Élévateur à godets industriel', - description: 'Machine de manutention verticale pour élévation de céréales.', - category: 'Triage & convoyage', - maintenanceFrequency: - "Inspection journalière des trappes, graissage hebdomadaire des paliers, contrôle courroie mensuel.", - specifications: { - zone: 'Réception', - instrumentation: ['Capteur vitesse', 'Capteur niveau'], - }, - customFields: [ - { name: 'Capacité nominale (t/h)', type: 'number', required: true }, - { name: 'Hauteur de levage (m)', type: 'number', required: true }, - { - name: 'Produit traité', - type: 'select', - options: ['Blé tendre', 'Orge brassicole', 'Maïs grain'], - defaultValue: 'Blé tendre', - required: true, - }, - { name: 'Date de mise en service', type: 'date', required: true }, - ], - componentRequirements: [ - { - label: "Tête d'élévateur", - typeCode: 'bucket-head-section', - minCount: 1, - maxCount: 1, - required: true, - }, - { - label: "Pied d'élévateur", - typeCode: 'bucket-boot-section', - minCount: 1, - maxCount: 1, - required: true, - }, - { - label: 'Tronçons de gaine', - typeCode: 'bucket-leg-section', - minCount: 2, - required: true, - }, - { - label: 'Motorisation principale', - typeCode: 'motor-drive', - minCount: 1, - required: true, - }, - { - label: 'Réducteur principal', - typeCode: 'gearbox-assembly', - minCount: 1, - required: true, - }, - { - label: 'Armoire locale', - typeCode: 'control-panel', - minCount: 1, - maxCount: 1, - required: false, - }, - ], - pieceRequirements: [ - { - label: 'Courroie élévatrice', - typeCode: 'drive-belt', - minCount: 1, - required: true, - }, - { - label: 'Capteur de vitesse', - typeCode: 'speed-sensor', - minCount: 1, - required: false, - }, - { - label: 'Kit visserie structure', - typeCode: 'hex-screw', - minCount: 1, - required: false, - }, - ], - }, - { - code: 'belt-conveyor', - name: 'Convoyeur à bande industriel', - description: 'Convoyeur horizontal ou incliné pour transfert de grains.', - category: 'Triage & convoyage', - maintenanceFrequency: 'Contrôle visuel quotidien, réglage de bande hebdomadaire, révision annuelle.', - specifications: { - zone: 'Transfert', - supports: ['Passerelle', 'Structure acier'], - }, - customFields: [ - { name: 'Débit (t/h)', type: 'number', required: true }, - { name: 'Longueur (m)', type: 'number', required: true }, - { - name: 'Localisation', - type: 'select', - options: ['Réception', 'Nettoyage', 'Expédition'], - defaultValue: 'Réception', - }, - ], - componentRequirements: [ - { - label: "Station d'entraînement", - typeCode: 'belt-drive-station', - minCount: 1, - maxCount: 1, - required: true, - }, - { - label: 'Station de retour', - typeCode: 'belt-tail-station', - minCount: 1, - maxCount: 1, - required: true, - }, - { - label: 'Ossature intermédiaire', - typeCode: 'belt-support-frame', - minCount: 1, - required: true, - }, - { - label: 'Motorisation convoyeur', - typeCode: 'motor-drive', - minCount: 1, - required: true, - }, - { - label: 'Réducteur convoyeur', - typeCode: 'gearbox-assembly', - minCount: 1, - required: true, - }, - { - label: 'Coffret de commande', - typeCode: 'control-panel', - minCount: 1, - required: false, - }, - ], - pieceRequirements: [ - { - label: 'Bande transporteuse', - typeCode: 'drive-belt', - minCount: 1, - required: true, - }, - { - label: 'Paliers supports', - typeCode: 'roller-bearing', - minCount: 2, - required: false, - }, - { - label: 'Visserie ossature', - typeCode: 'hex-screw', - minCount: 1, - required: false, - }, - ], - }, - { - code: 'gravity-separator', - name: 'Table densimétrique', - description: 'Table vibrante pour séparation par densité.', - category: 'Nettoyage', - maintenanceFrequency: 'Nettoyage tamis quotidien, contrôle vibrateurs hebdomadaire.', - specifications: { - zone: 'Nettoyage fin', - aspiration: true, - }, - customFields: [ - { name: 'Capacité de tri (t/h)', type: 'number', required: true }, - { - name: 'Produit traité', - type: 'select', - options: ['Blé', 'Orge', 'Tournesol'], - defaultValue: 'Blé', - }, - { name: 'Date de mise en service', type: 'date', required: true }, - ], - componentRequirements: [ - { - label: 'Plateau vibrant', - typeCode: 'gravity-vibration-deck', - minCount: 1, - required: true, - }, - { - label: 'Ventilateur aspiration', - typeCode: 'ventilation-fan', - minCount: 1, - required: true, - }, - { - label: 'Armoire de réglage', - typeCode: 'control-panel', - minCount: 1, - required: true, - }, - ], - pieceRequirements: [ - { - label: 'Capteur vibration', - typeCode: 'speed-sensor', - minCount: 1, - required: false, - }, - { - label: 'Kit visserie plateau', - typeCode: 'hex-screw', - minCount: 1, - required: false, - }, - { - label: 'Cartouche aspiration', - typeCode: 'filter-cartridge', - minCount: 1, - required: false, - }, - ], - }, - { - code: 'grain-dryer', - name: 'Séchoir à grains continu', - description: 'Système de séchage continu haute capacité.', - category: 'Séchage', - maintenanceFrequency: - 'Contrôle brûleur hebdomadaire, nettoyage filtre et vérification ventilateurs mensuels.', - specifications: { - bâtiment: 'Tour de séchage', - energie: ['Gaz naturel', 'Électricité'], - }, - customFields: [ - { name: 'Capacité sèche (t/h)', type: 'number', required: true }, - { - name: 'Mode d’alimentation', - type: 'select', - options: ['Élévateur principal', 'Vis de reprise'], - defaultValue: 'Élévateur principal', - }, - { name: 'Date de mise en service', type: 'date', required: true }, - ], - componentRequirements: [ - { - label: 'Module brûleur', - typeCode: 'burner-module', - minCount: 1, - required: true, - }, - { - label: 'Colonnes de séchage', - typeCode: 'dryer-column-segment', - minCount: 2, - required: true, - }, - { - label: 'Ventilation process', - typeCode: 'ventilation-fan', - minCount: 1, - required: true, - }, - { - label: 'Filtre poussières', - typeCode: 'dust-filter', - minCount: 1, - required: true, - }, - { - label: 'Armoire de pilotage', - typeCode: 'control-panel', - minCount: 1, - required: true, - }, - ], - pieceRequirements: [ - { - label: 'Sondes température', - typeCode: 'temperature-probe', - minCount: 2, - required: false, - }, - { - label: 'Joints haute température', - typeCode: 'flat-gasket', - minCount: 1, - required: false, - }, - { - label: 'Cartouches dépoussiéreur', - typeCode: 'filter-cartridge', - minCount: 2, - required: false, - }, - ], - }, - { - code: 'screw-conveyor', - name: 'Vis de reprise céréales', - description: 'Vis sans fin pour reprise et rechargement des silos.', - category: 'Convoyage', - maintenanceFrequency: 'Graissage paliers hebdomadaire, inspection trémie mensuelle.', - specifications: { - position: ['Sous cellules', 'Sortie séchoir'], - }, - customFields: [ - { name: 'Diamètre vis (mm)', type: 'number', required: true }, - { name: 'Inclinaison (°)', type: 'number' }, - { name: 'Vitesse (rpm)', type: 'number' }, - ], - componentRequirements: [ - { - label: 'Caisson principal', - typeCode: 'screw-trough-section', - minCount: 1, - required: true, - }, - { - label: "Trémie d'alimentation", - typeCode: 'screw-inlet-hopper', - minCount: 1, - required: true, - }, - { - label: 'Goulotte de sortie', - typeCode: 'screw-outlet-chute', - minCount: 1, - required: true, - }, - { - label: 'Motorisation vis', - typeCode: 'motor-drive', - minCount: 1, - required: true, - }, - { - label: 'Réducteur vis', - typeCode: 'gearbox-assembly', - minCount: 1, - required: true, - }, - ], - pieceRequirements: [ - { - label: 'Paliers de ligne', - typeCode: 'roller-bearing', - minCount: 2, - required: false, - }, - { - label: 'Kit visserie caisson', - typeCode: 'hex-screw', - minCount: 1, - required: false, - }, - { - label: 'Cartouches de graisse', - typeCode: 'lubrication-cartridge', - minCount: 1, - required: false, - }, - ], - }, - { - code: 'weigh-hopper', - name: 'Benne peseuse', - description: 'Benne peseuse pour chargement camions ou big-bags.', - category: 'Expédition', - maintenanceFrequency: 'Vérification capteurs hebdomadaire, recalibrage trimestriel.', - specifications: { - utilisation: ['Chargement camion', 'Conditionnement big-bag'], - }, - customFields: [ - { name: 'Capacité de pesée (kg)', type: 'number', required: true }, - { - name: 'Précision (%)', - type: 'number', - required: true, - }, - { - name: 'Mode de vidange', - type: 'select', - options: ['Trappe motorisée', 'Vis doseuse', 'Vanne guillotine'], - defaultValue: 'Trappe motorisée', - }, - ], - componentRequirements: [ - { - label: 'Cadre peseur', - typeCode: 'weigh-load-frame', - minCount: 1, - required: true, - }, - { - label: 'Vanne de vidange', - typeCode: 'weigh-discharge-gate', - minCount: 1, - required: true, - }, - { - label: 'Coffret de pesage', - typeCode: 'control-panel', - minCount: 1, - required: true, - }, - ], - pieceRequirements: [ - { - label: 'Capteurs de pesage', - typeCode: 'load-cell', - minCount: 2, - required: true, - }, - { - label: 'Fusibles de protection', - typeCode: 'fuse-cartridge', - minCount: 1, - required: false, - }, - { - label: 'Joint trappe', - typeCode: 'flat-gasket', - minCount: 1, - required: false, - }, - ], - }, - { - code: 'telehandler', - name: 'Chariot télescopique logistique', - description: 'Chariot télescopique dédié aux manutentions céréalières.', - category: 'Logistique', - maintenanceFrequency: 'Graissage hebdomadaire, contrôle hydraulique mensuel.', - specifications: { - yardArea: 'Cour logistique', - rotation: '3 équipes', - }, - customFields: [ - { - name: 'Usage principal', - type: 'select', - options: ['Chargement camions', 'Gestion big-bags', 'Maintenance silo'], - defaultValue: 'Chargement camions', - }, - { - name: 'Lieu de stationnement', - type: 'select', - options: ['Hangar nord', 'Cour extérieure', 'Atelier maintenance'], - defaultValue: 'Hangar nord', - }, - { name: "Année d'achat", type: 'number', required: true }, - ], - componentRequirements: [ - { - label: 'Flèche télescopique', - typeCode: 'telehandler-boom', - minCount: 1, - required: true, - }, - { - label: 'Groupe hydraulique', - typeCode: 'hydraulic-power-pack', - minCount: 1, - required: true, - }, - { - label: 'Groupe moteur', - typeCode: 'motor-drive', - minCount: 1, - required: true, - }, - { - label: 'Cabine opérateur', - typeCode: 'telehandler-cab-module', - minCount: 1, - required: true, - }, - { - label: 'Support d’outils', - typeCode: 'telehandler-attachment-carrier', - minCount: 1, - required: true, - }, - ], - pieceRequirements: [ - { - label: 'Flexibles hydrauliques', - typeCode: 'hydraulic-hose', - minCount: 2, - required: false, - }, - { - label: 'Fusibles cabine', - typeCode: 'fuse-cartridge', - minCount: 1, - required: false, - }, - { - label: 'Cartouches graissage', - typeCode: 'lubrication-cartridge', - minCount: 1, - required: false, - }, - ], - }, -]; - -const machineBuilds: MachineBuildSpec[] = [ - { - code: 'bucket-elevator-upstream', - typeMachineCode: 'bucket-elevator', - name: 'Élévateur amont Z400', - reference: 'BE-Z400-01', - prix: '58000', - constructeurKey: 'agritech', - customFieldValues: { - 'Capacité nominale (t/h)': 120, - 'Hauteur de levage (m)': 38, - 'Produit traité': 'Blé tendre', - 'Date de mise en service': new Date('2023-10-12'), - }, - components: [ - { - name: 'Tête haute Z400', - reference: 'BE-TETE-400', - prix: '18500', - typeCode: 'bucket-head-section', - requirementLabel: "Tête d'élévateur", - modelCode: 'bucket-head-120', - constructeur: 'agritech', - customValues: { - 'Largeur tambour (mm)': 820, - 'Type de revêtement': 'Caoutchouc rainuré', - 'Nombre de trappes': 3, - }, - pieces: [ - { - name: 'Kit visserie tête', - reference: 'KIT-TETE-01', - typeCode: 'hex-screw', - modelCode: 'screw-m12x80', - customValues: { - 'Diamètre (mm)': 12, - 'Longueur (mm)': 80, - 'Classe acier': '8.8', - }, - }, - { - name: 'Rondelles de sécurité Ø12', - reference: 'RDL-TETE-01', - typeCode: 'lock-washer', - modelCode: 'washer-grower-12', - customValues: { - 'Diamètre (mm)': 12, - Finition: 'Inox', - }, - }, - ], - children: [ - { - name: 'Motorisation 75 kW', - reference: 'MTR-75-EL1', - typeCode: 'motor-drive', - requirementLabel: 'Motorisation principale', - modelCode: 'motor-drive-75', - constructeur: 'sew', - customValues: { - 'Puissance nominale (kW)': 75, - 'Classe énergétique': 'IE3', - "Indice de protection": 'IP55', - }, - }, - { - name: 'Réducteur Flender 3200', - reference: 'GBX-FL-01', - typeCode: 'gearbox-assembly', - requirementLabel: 'Réducteur principal', - modelCode: 'gearbox-flender', - constructeur: 'flender', - customValues: { - 'Rapport de réduction': '1:28', - 'Couple nominal (Nm)': 3200, - 'Type de montage': 'À bride', - }, - }, - ], - }, - { - name: 'Pied élévateur renforcé', - reference: 'BE-PIED-400', - prix: '14600', - typeCode: 'bucket-boot-section', - requirementLabel: "Pied d'élévateur", - modelCode: 'bucket-boot-heavy', - constructeur: 'agritech', - customValues: { - 'Capacité trémie (L)': 480, - 'Mode de tension': 'Vis manuelle', - 'Système de nettoyage': 'Grattoirs', - }, - pieces: [ - { - name: 'Joint trappe inspection', - reference: 'JNT-PIED-01', - typeCode: 'flat-gasket', - modelCode: 'gasket-ht-200', - customValues: { - Matière: 'Fibre compressée', - 'Épaisseur (mm)': 3, - }, - }, - ], - }, - { - name: 'Gaine intermédiaire 3 m - A', - reference: 'BE-GAINE-1', - typeCode: 'bucket-leg-section', - requirementLabel: 'Tronçons de gaine', - modelCode: 'bucket-leg-3m', - constructeur: 'agritech', - customValues: { - 'Hauteur section (m)': 3, - 'Type de gaine': 'Boulonnée', - }, - }, - { - name: 'Gaine intermédiaire 3 m - B', - reference: 'BE-GAINE-2', - typeCode: 'bucket-leg-section', - requirementLabel: 'Tronçons de gaine', - modelCode: 'bucket-leg-3m', - constructeur: 'agritech', - customValues: { - 'Hauteur section (m)': 3, - 'Type de gaine': 'Renforcée', - }, - }, - { - name: 'Armoire locale tête élévateur', - reference: 'ARM-EL-01', - prix: '7800', - typeCode: 'control-panel', - requirementLabel: 'Armoire locale', - modelCode: 'control-panel-m340', - constructeur: 'buhler', - customValues: { - 'Automate principal': 'Schneider Modicon M340', - 'Année de mise à jour': 2023, - "Indice de protection": 'IP55', - }, - pieces: [ - { - name: 'Fusible alimentation 25A', - reference: 'FS-25A-EL', - typeCode: 'fuse-cartridge', - modelCode: 'fuse-gg-25a', - customValues: { - 'Calibre (A)': 25, - Type: 'gG', - }, - }, - ], - }, - ], - sparePieces: [ - { - name: 'Courroie de rechange 800 mm', - reference: 'SP-BELT-800', - prix: '2200', - typeCode: 'drive-belt', - modelCode: 'belt-hd-800', - constructeur: 'agritech', - requirementLabel: 'Courroie élévatrice', - customValues: { - 'Largeur (mm)': 800, - Matériau: 'Caoutchouc nitrile', - }, - }, - { - name: 'Capteur vitesse secours', - reference: 'SP-SPEED-01', - typeCode: 'speed-sensor', - modelCode: 'sensor-speed-m12', - constructeur: 'ifm', - requirementLabel: 'Capteur de vitesse', - customValues: { - 'Type de sortie': 'PNP 4-20 mA', - 'Plage de mesure (rpm)': 1200, - }, - }, - { - name: 'Jeu visserie structure', - reference: 'SP-VISS-EL', - typeCode: 'hex-screw', - modelCode: 'screw-m12x80', - requirementLabel: 'Kit visserie structure', - customValues: { - 'Diamètre (mm)': 12, - 'Longueur (mm)': 80, - 'Classe acier': '8.8', - }, - }, - ], - }, - { - code: 'bucket-elevator-downstream', - typeMachineCode: 'bucket-elevator', - name: 'Élévateur aval Z320', - reference: 'BE-Z320-02', - prix: '49700', - constructeurKey: 'agritech', - customFieldValues: { - 'Capacité nominale (t/h)': 95, - 'Hauteur de levage (m)': 32, - 'Produit traité': 'Orge brassicole', - 'Date de mise en service': new Date('2022-09-05'), - }, - components: [ - { - name: 'Tête compact Z320', - reference: 'BE-TETE-320', - typeCode: 'bucket-head-section', - requirementLabel: "Tête d'élévateur", - modelCode: 'bucket-head-95', - constructeur: 'agritech', - customValues: { - 'Largeur tambour (mm)': 680, - 'Type de revêtement': 'Céramique', - 'Nombre de trappes': 2, - }, - children: [ - { - name: 'Motorisation 55 kW', - reference: 'MTR-55-EL2', - typeCode: 'motor-drive', - requirementLabel: 'Motorisation principale', - modelCode: 'motor-drive-55', - constructeur: 'sew', - customValues: { - 'Puissance nominale (kW)': 55, - 'Classe énergétique': 'IE3', - "Indice de protection": 'IP55', - }, - }, - { - name: 'Réducteur Bonfiglioli', - reference: 'GBX-BF-02', - typeCode: 'gearbox-assembly', - requirementLabel: 'Réducteur principal', - modelCode: 'gearbox-bonfiglioli', - constructeur: 'bonfiglioli', - customValues: { - 'Rapport de réduction': '1:24', - 'Couple nominal (Nm)': 2100, - 'Type de montage': 'Sur arbre', - }, - }, - ], - }, - { - name: 'Pied élévateur compact', - reference: 'BE-PIED-320', - typeCode: 'bucket-boot-section', - requirementLabel: "Pied d'élévateur", - modelCode: 'bucket-boot-compact', - constructeur: 'agritech', - customValues: { - 'Capacité trémie (L)': 360, - 'Mode de tension': 'Hydraulique', - 'Système de nettoyage': 'Vide-pied', - }, - }, - { - name: 'Gaine intermédiaire 2,5 m', - reference: 'BE-GAINE-3', - typeCode: 'bucket-leg-section', - requirementLabel: 'Tronçons de gaine', - modelCode: 'bucket-leg-2-5m', - constructeur: 'agritech', - customValues: { - 'Hauteur section (m)': 2.5, - 'Type de gaine': 'Boulonnée', - }, - }, - { - name: 'Gaine renfort 3 m', - reference: 'BE-GAINE-4', - typeCode: 'bucket-leg-section', - requirementLabel: 'Tronçons de gaine', - modelCode: 'bucket-leg-3m', - constructeur: 'agritech', - customValues: { - 'Hauteur section (m)': 3, - 'Type de gaine': 'Renforcée', - }, - }, - { - name: 'Coffret local aval', - reference: 'ARM-EL-02', - typeCode: 'control-panel', - requirementLabel: 'Armoire locale', - modelCode: 'control-panel-s7', - constructeur: 'buhler', - customValues: { - 'Automate principal': 'Siemens S7-1500', - 'Année de mise à jour': 2022, - "Indice de protection": 'IP65', - }, - }, - ], - sparePieces: [ - { - name: 'Courroie de secours 650 mm', - reference: 'SP-BELT-650', - typeCode: 'drive-belt', - modelCode: 'belt-hd-650', - requirementLabel: 'Courroie élévatrice', - customValues: { - 'Largeur (mm)': 650, - Matériau: 'Polyuréthane', - }, - }, - { - name: 'Capteur vitesse compact', - reference: 'SP-SPEED-02', - typeCode: 'speed-sensor', - modelCode: 'sensor-speed-m18', - constructeur: 'ifm', - requirementLabel: 'Capteur de vitesse', - customValues: { - 'Type de sortie': 'PNP Tout ou rien', - 'Plage de mesure (rpm)': 900, - }, - }, - { - name: 'Kit visserie élévateur aval', - reference: 'SP-VISS-EL2', - typeCode: 'hex-screw', - modelCode: 'screw-m10x60', - requirementLabel: 'Kit visserie structure', - customValues: { - 'Diamètre (mm)': 10, - 'Longueur (mm)': 60, - 'Classe acier': '10.9', - }, - }, - ], - }, - { - code: 'belt-conveyor-feed', - typeMachineCode: 'belt-conveyor', - name: "Convoyeur alimentation 18 m", - reference: 'CV-ALIM-18', - prix: '32600', - constructeurKey: 'valmont', - customFieldValues: { - 'Débit (t/h)': 110, - 'Longueur (m)': 18, - Localisation: 'Réception', - }, - components: [ - { - name: "Station d'entraînement 800", - reference: 'CV-DRIVE-18', - typeCode: 'belt-drive-station', - requirementLabel: "Station d'entraînement", - modelCode: 'belt-drive-800', - constructeur: 'valmont', - customValues: { - 'Type de tambour': 'Caoutchouc rainuré', - 'Système de tension': 'Hydraulique', - }, - children: [ - { - name: 'Moteur 45 kW', - reference: 'MTR-45-CV1', - typeCode: 'motor-drive', - requirementLabel: 'Motorisation convoyeur', - modelCode: 'motor-drive-45', - constructeur: 'sew', - customValues: { - 'Puissance nominale (kW)': 45, - 'Classe énergétique': 'IE3', - "Indice de protection": 'IP55', - }, - }, - { - name: 'Réducteur Bonfiglioli TA', - reference: 'GBX-TA-CV1', - typeCode: 'gearbox-assembly', - requirementLabel: 'Réducteur convoyeur', - modelCode: 'gearbox-bonfiglioli', - constructeur: 'bonfiglioli', - customValues: { - 'Rapport de réduction': '1:24', - 'Couple nominal (Nm)': 2100, - 'Type de montage': 'Sur arbre', - }, - }, - ], - }, - { - name: 'Station de retour 800', - reference: 'CV-TAIL-18', - typeCode: 'belt-tail-station', - requirementLabel: 'Station de retour', - modelCode: 'belt-tail-800', - constructeur: 'valmont', - customValues: { - 'Nettoyeur principal': 'Racleur PU', - 'Diamètre tambour (mm)': 420, - }, - pieces: [ - { - name: 'Grattoir principal PU', - reference: 'GRAT-800-01', - typeCode: 'flat-gasket', - modelCode: 'gasket-ht-200', - customValues: { - Matière: 'NBR', - 'Épaisseur (mm)': 5, - }, - }, - ], - }, - { - name: 'Ossature 18 m', - reference: 'CV-FRAME-18', - typeCode: 'belt-support-frame', - requirementLabel: 'Ossature intermédiaire', - modelCode: 'belt-frame-18m', - constructeur: 'valmont', - customValues: { - 'Longueur (m)': 18, - 'Nombre de rouleaux': 36, - 'Type de châssis': 'Treillis', - }, - pieces: [ - { - name: 'Roulement UCP210', - reference: 'BRG-UCP210-02', - typeCode: 'roller-bearing', - modelCode: 'bearing-ucp210', - constructeur: 'skf', - customValues: { - Série: 'UCP', - "Type d'étanchéité": '2RS', - }, - }, - ], - }, - { - name: 'Coffret convoyeur réception', - reference: 'ARM-CV-01', - typeCode: 'control-panel', - requirementLabel: 'Coffret de commande', - modelCode: 'control-panel-m340', - constructeur: 'buhler', - customValues: { - 'Automate principal': 'Schneider Modicon M340', - "Indice de protection": 'IP55', - }, - }, - ], - sparePieces: [ - { - name: 'Bande transporteuse 800 mm', - reference: 'SP-BAND-800', - typeCode: 'drive-belt', - modelCode: 'belt-hd-800', - requirementLabel: 'Bande transporteuse', - customValues: { - 'Largeur (mm)': 800, - Matériau: 'Caoutchouc nitrile', - }, - }, - { - name: 'Kit palier de rechange', - reference: 'SP-BRG-800', - typeCode: 'roller-bearing', - modelCode: 'bearing-ucp210', - requirementLabel: 'Paliers supports', - constructeur: 'skf', - customValues: { - Série: 'UCP', - "Type d'étanchéité": '2RS', - }, - }, - { - name: 'Visserie ossature', - reference: 'SP-VISS-CV1', - typeCode: 'hex-screw', - modelCode: 'screw-m10x60', - requirementLabel: 'Visserie ossature', - customValues: { - 'Diamètre (mm)': 10, - 'Longueur (mm)': 60, - 'Classe acier': '10.9', - }, - }, - ], - }, - { - code: 'belt-conveyor-expedition', - typeMachineCode: 'belt-conveyor', - name: 'Convoyeur expédition 25 m', - reference: 'CV-EXP-25', - prix: '35400', - constructeurKey: 'valmont', - customFieldValues: { - 'Débit (t/h)': 90, - 'Longueur (m)': 25, - Localisation: 'Expédition', - }, - components: [ - { - name: 'Station tête 650', - reference: 'CV-DRIVE-25', - typeCode: 'belt-drive-station', - requirementLabel: "Station d'entraînement", - modelCode: 'belt-drive-650', - constructeur: 'valmont', - customValues: { - 'Type de tambour': 'Céramique', - 'Système de tension': 'Vis manuelle', - }, - children: [ - { - name: 'Moteur 37 kW', - reference: 'MTR-37-CV2', - typeCode: 'motor-drive', - requirementLabel: 'Motorisation convoyeur', - modelCode: 'motor-drive-37', - constructeur: 'sew', - customValues: { - 'Puissance nominale (kW)': 37, - 'Classe énergétique': 'IE3', - "Indice de protection": 'IP55', - }, - }, - { - name: 'Réducteur SEW K', - reference: 'GBX-SEW-CV2', - typeCode: 'gearbox-assembly', - requirementLabel: 'Réducteur convoyeur', - modelCode: 'gearbox-sew', - constructeur: 'sew', - customValues: { - 'Rapport de réduction': '1:18', - 'Couple nominal (Nm)': 1800, - 'Type de montage': 'À pattes', - }, - }, - ], - }, - { - name: 'Station retour 650', - reference: 'CV-TAIL-25', - typeCode: 'belt-tail-station', - requirementLabel: 'Station de retour', - modelCode: 'belt-tail-650', - constructeur: 'valmont', - customValues: { - 'Nettoyeur principal': 'Brosse acier', - 'Diamètre tambour (mm)': 360, - }, - }, - { - name: 'Ossature 25 m', - reference: 'CV-FRAME-25', - typeCode: 'belt-support-frame', - requirementLabel: 'Ossature intermédiaire', - modelCode: 'belt-frame-25m', - constructeur: 'valmont', - customValues: { - 'Longueur (m)': 25, - 'Nombre de rouleaux': 48, - 'Type de châssis': 'Portique', - }, - }, - { - name: 'Coffret expédition', - reference: 'ARM-CV-02', - typeCode: 'control-panel', - requirementLabel: 'Coffret de commande', - modelCode: 'control-panel-s7', - constructeur: 'buhler', - customValues: { - 'Automate principal': 'Siemens S7-1500', - 'Année de mise à jour': 2021, - "Indice de protection": 'IP65', - }, - }, - ], - sparePieces: [ - { - name: 'Bande transporteuse 650 mm', - reference: 'SP-BAND-650', - typeCode: 'drive-belt', - modelCode: 'belt-hd-650', - requirementLabel: 'Bande transporteuse', - customValues: { - 'Largeur (mm)': 650, - Matériau: 'Polyuréthane', - }, - }, - { - name: 'Palier UCFL207', - reference: 'SP-BRG-UCFL', - typeCode: 'roller-bearing', - modelCode: 'bearing-ucfl207', - requirementLabel: 'Paliers supports', - constructeur: 'skf', - customValues: { - Série: 'UCFL', - "Type d'étanchéité": 'ZZ', - }, - }, - { - name: 'Visserie tendeurs', - reference: 'SP-VISS-CV2', - typeCode: 'hex-screw', - modelCode: 'screw-m8x30', - requirementLabel: 'Visserie ossature', - customValues: { - 'Diamètre (mm)': 8, - 'Longueur (mm)': 30, - 'Classe acier': '8.8', - }, - }, - ], - }, - { - code: 'gravity-table-main', - typeMachineCode: 'gravity-separator', - name: 'Table densimétrique TQX', - reference: 'TBL-TQX-01', - prix: '68500', - constructeurKey: 'buhler', - customFieldValues: { - 'Capacité de tri (t/h)': 120, - 'Produit traité': 'Blé', - 'Date de mise en service': new Date('2021-03-22'), - }, - components: [ - { - name: 'Plateau TQX-120', - reference: 'TBL-PLT-01', - typeCode: 'gravity-vibration-deck', - requirementLabel: 'Plateau vibrant', - modelCode: 'gravity-deck-120', - constructeur: 'buhler', - customValues: { - 'Type de plateau': 'Acier perforé', - 'Fréquence de vibration (Hz)': 45, - 'Inclinaison plateau (°)': 5, - }, - children: [ - { - name: 'Moteur vibrateur 18,5 kW', - reference: 'MTR-18-VIB', - typeCode: 'motor-drive', - modelCode: 'motor-drive-18', - constructeur: 'sew', - customValues: { - 'Puissance nominale (kW)': 18.5, - 'Classe énergétique': 'IE3', - "Indice de protection": 'IP55', - }, - }, - ], - }, - { - name: 'Ventilateur aspiration 45 kW', - reference: 'VENT-45-01', - typeCode: 'ventilation-fan', - requirementLabel: 'Ventilateur aspiration', - modelCode: 'fan-process-45', - constructeur: 'agrifan', - customValues: { - 'Débit (m³/h)': 45000, - 'Type de roue': 'Centrifuge', - 'Vitesse nominale (rpm)': 1450, - }, - }, - { - name: 'Armoire réglage table', - reference: 'ARM-TBL-01', - typeCode: 'control-panel', - requirementLabel: 'Armoire de réglage', - modelCode: 'control-panel-m340', - constructeur: 'buhler', - customValues: { - 'Automate principal': 'Schneider Modicon M340', - 'Année de mise à jour': 2023, - "Indice de protection": 'IP55', - }, - }, - ], - sparePieces: [ - { - name: 'Capteur vibration M12', - reference: 'SP-SNS-VIB', - typeCode: 'speed-sensor', - modelCode: 'sensor-speed-m12', - requirementLabel: 'Capteur vibration', - customValues: { - 'Type de sortie': 'PNP 4-20 mA', - 'Plage de mesure (rpm)': 1500, - }, - }, - { - name: 'Kit visserie plateau', - reference: 'SP-VISS-TBL', - typeCode: 'hex-screw', - modelCode: 'screw-m8x30', - requirementLabel: 'Kit visserie plateau', - customValues: { - 'Diamètre (mm)': 8, - 'Longueur (mm)': 30, - 'Classe acier': '8.8', - }, - }, - { - name: 'Cartouche aspiration 610', - reference: 'SP-FILT-610', - typeCode: 'filter-cartridge', - modelCode: 'filter-dust-610', - requirementLabel: 'Cartouche aspiration', - customValues: { - 'Longueur (mm)': 610, - 'Média filtrant': 'Polyester', - }, - }, - ], - }, - { - code: 'grain-dryer-main', - typeMachineCode: 'grain-dryer', - name: 'Séchoir continu SC-60', - reference: 'DRY-SC60', - prix: '212000', - constructeurKey: 'agridry', - customFieldValues: { - 'Capacité sèche (t/h)': 60, - 'Mode d’alimentation': 'Élévateur principal', - 'Date de mise en service': new Date('2020-07-15'), - }, - components: [ - { - name: 'Brûleur gaz 3 MW', - reference: 'BRN-3MW-01', - typeCode: 'burner-module', - requirementLabel: 'Module brûleur', - modelCode: 'burner-module-3mw', - constructeur: 'agridry', - customValues: { - 'Puissance thermique (kW)': 3000, - 'Type de carburant': 'Gaz naturel', - "Système d'allumage": 'Double électrode', - }, - }, - { - name: 'Colonne zone chaude', - reference: 'COL-CHAUD-01', - typeCode: 'dryer-column-segment', - requirementLabel: 'Colonnes de séchage', - modelCode: 'dryer-column-2m', - constructeur: 'agridry', - customValues: { - 'Hauteur segment (m)': 2, - 'Zone de séchage': 'Échauffage', - 'Capteurs intégrés': true, - }, - }, - { - name: 'Colonne zone tempérée', - reference: 'COL-TEMP-01', - typeCode: 'dryer-column-segment', - requirementLabel: 'Colonnes de séchage', - modelCode: 'dryer-column-1-5m', - constructeur: 'agridry', - customValues: { - 'Hauteur segment (m)': 1.5, - 'Zone de séchage': 'Tempe', - 'Capteurs intégrés': true, - }, - }, - { - name: 'Ventilateur process principal', - reference: 'VENT-45-DRY', - typeCode: 'ventilation-fan', - requirementLabel: 'Ventilation process', - modelCode: 'fan-process-45', - constructeur: 'agrifan', - customValues: { - 'Débit (m³/h)': 52000, - 'Type de roue': 'Centrifuge', - 'Vitesse nominale (rpm)': 1480, - }, - children: [ - { - name: 'Motorisation ventilateur 110 kW', - reference: 'MTR-110-VENT', - typeCode: 'motor-drive', - modelCode: 'motor-drive-110', - constructeur: 'sew', - customValues: { - 'Puissance nominale (kW)': 110, - 'Classe énergétique': 'IE3', - "Indice de protection": 'IP55', - }, - }, - ], - }, - { - name: 'Filtre cyclone 610', - reference: 'FIL-CYCL-01', - typeCode: 'dust-filter', - requirementLabel: 'Filtre poussières', - modelCode: 'dust-filter-cyclone', - constructeur: 'agrifan', - customValues: { - 'Efficacité de filtration (%)': 98, - 'Type de média filtrant': 'Polyester', - 'Nombre de cartouches': 6, - }, - pieces: [ - { - name: 'Cartouche polyester 610', - reference: 'FILT-610-01', - typeCode: 'filter-cartridge', - modelCode: 'filter-dust-610', - customValues: { - 'Longueur (mm)': 610, - 'Média filtrant': 'Polyester', - }, - }, - ], - }, - { - name: 'Armoire pilotage séchoir', - reference: 'ARM-DRY-01', - typeCode: 'control-panel', - requirementLabel: 'Armoire de pilotage', - modelCode: 'control-panel-m340', - constructeur: 'buhler', - customValues: { - 'Automate principal': 'Schneider Modicon M340', - 'Année de mise à jour': 2024, - "Indice de protection": 'IP55', - }, - }, - ], - sparePieces: [ - { - name: 'Sonde PT100 longue', - reference: 'SP-PT100-01', - typeCode: 'temperature-probe', - modelCode: 'temp-probe-pt100', - requirementLabel: 'Sondes température', - customValues: { - Type: 'PT100 classe A', - 'Longueur tige (mm)': 300, - }, - }, - { - name: 'Sonde PT100 courte', - reference: 'SP-PT100-02', - typeCode: 'temperature-probe', - modelCode: 'temp-probe-pt100-short', - requirementLabel: 'Sondes température', - customValues: { - Type: 'PT100 classe B', - 'Longueur tige (mm)': 200, - }, - }, - { - name: 'Joint trappe brûleur', - reference: 'SP-JOINT-DRY', - typeCode: 'flat-gasket', - modelCode: 'gasket-ht-200', - requirementLabel: 'Joints haute température', - customValues: { - Matière: 'Fibre compressée', - 'Épaisseur (mm)': 4, - }, - }, - { - name: 'Cartouche rechange 480', - reference: 'SP-FILT-480', - typeCode: 'filter-cartridge', - modelCode: 'filter-dust-480', - requirementLabel: 'Cartouches dépoussiéreur', - customValues: { - 'Longueur (mm)': 480, - 'Média filtrant': 'Polyester', - }, - }, - ], - }, - { - code: 'screw-conveyor-north', - typeMachineCode: 'screw-conveyor', - name: 'Vis de reprise nord V200', - reference: 'SC-V200-N', - prix: '27400', - constructeurKey: 'valmont', - customFieldValues: { - 'Diamètre vis (mm)': 200, - 'Inclinaison (°)': 12, - 'Vitesse (rpm)': 140, - }, - components: [ - { - name: 'Caisson principal 200', - reference: 'SC-CAISSON-200', - typeCode: 'screw-trough-section', - requirementLabel: 'Caisson principal', - modelCode: 'screw-trough-200', - constructeur: 'valmont', - customValues: { - 'Longueur (m)': 9, - 'Diamètre vis (mm)': 200, - Matériau: 'Acier peint', - }, - }, - { - name: "Trémie d'alimentation 200", - reference: 'SC-TREMIE-200', - typeCode: 'screw-inlet-hopper', - requirementLabel: "Trémie d'alimentation", - modelCode: 'screw-inlet-200', - constructeur: 'valmont', - customValues: { - 'Capacité (L)': 220, - 'Type de grille': 'Grille anti-gravats', - }, - }, - { - name: 'Goulotte sortie vanne', - reference: 'SC-GOUL-200', - typeCode: 'screw-outlet-chute', - requirementLabel: 'Goulotte de sortie', - modelCode: 'screw-outlet-vanne', - constructeur: 'valmont', - customValues: { - 'Type de vanne': 'Guillotine', - "Orientation (°)": 45, - }, - }, - { - name: 'Moteur vis 18.5 kW', - reference: 'MTR-18-SC1', - typeCode: 'motor-drive', - requirementLabel: 'Motorisation vis', - modelCode: 'motor-drive-18', - constructeur: 'sew', - customValues: { - 'Puissance nominale (kW)': 18.5, - 'Classe énergétique': 'IE3', - "Indice de protection": 'IP55', - }, - }, - { - name: 'Réducteur SEW K', - reference: 'GBX-SEW-SC1', - typeCode: 'gearbox-assembly', - requirementLabel: 'Réducteur vis', - modelCode: 'gearbox-sew', - constructeur: 'sew', - customValues: { - 'Rapport de réduction': '1:32', - 'Couple nominal (Nm)': 1800, - 'Type de montage': 'À pattes', - }, - }, - ], - sparePieces: [ - { - name: 'Palier UCP210', - reference: 'SP-PALIER-200', - typeCode: 'roller-bearing', - modelCode: 'bearing-ucp210', - requirementLabel: 'Paliers de ligne', - constructeur: 'skf', - customValues: { - Série: 'UCP', - "Type d'étanchéité": '2RS', - }, - }, - { - name: 'Kit visserie caisson', - reference: 'SP-VISS-SC1', - typeCode: 'hex-screw', - modelCode: 'screw-m12x80', - requirementLabel: 'Kit visserie caisson', - customValues: { - 'Diamètre (mm)': 12, - 'Longueur (mm)': 80, - 'Classe acier': '8.8', - }, - }, - { - name: 'Cartouche graisse 400g', - reference: 'SP-GRAISSE-01', - typeCode: 'lubrication-cartridge', - modelCode: 'grease-cartridge-400', - requirementLabel: 'Cartouches de graisse', - customValues: { - 'Volume (cm³)': 400, - 'Grade de graisse': 'NLGI 2', - }, - }, - ], - }, - { - code: 'screw-conveyor-south', - typeMachineCode: 'screw-conveyor', - name: 'Vis de reprise sud V160', - reference: 'SC-V160-S', - prix: '23600', - constructeurKey: 'valmont', - customFieldValues: { - 'Diamètre vis (mm)': 160, - 'Inclinaison (°)': 8, - 'Vitesse (rpm)': 110, - }, - components: [ - { - name: 'Caisson principal 160', - reference: 'SC-CAISSON-160', - typeCode: 'screw-trough-section', - requirementLabel: 'Caisson principal', - modelCode: 'screw-trough-160', - constructeur: 'valmont', - customValues: { - 'Longueur (m)': 7, - 'Diamètre vis (mm)': 160, - Matériau: 'Galvanisé', - }, - }, - { - name: "Trémie d'alimentation 160", - reference: 'SC-TREMIE-160', - typeCode: 'screw-inlet-hopper', - requirementLabel: "Trémie d'alimentation", - modelCode: 'screw-inlet-160', - constructeur: 'valmont', - customValues: { - 'Capacité (L)': 160, - 'Type de grille': 'Grille magnétique', - }, - }, - { - name: 'Goulotte clapet', - reference: 'SC-GOUL-160', - typeCode: 'screw-outlet-chute', - requirementLabel: 'Goulotte de sortie', - modelCode: 'screw-outlet-flap', - constructeur: 'valmont', - customValues: { - 'Type de vanne': 'By-pass', - "Orientation (°)": 60, - }, - }, - { - name: 'Moteur vis 15 kW', - reference: 'MTR-15-SC2', - typeCode: 'motor-drive', - requirementLabel: 'Motorisation vis', - modelCode: 'motor-drive-15', - constructeur: 'sew', - customValues: { - 'Puissance nominale (kW)': 15, - 'Classe énergétique': 'IE3', - "Indice de protection": 'IP55', - }, - }, - { - name: 'Réducteur Bonfiglioli', - reference: 'GBX-BF-SC2', - typeCode: 'gearbox-assembly', - requirementLabel: 'Réducteur vis', - modelCode: 'gearbox-bonfiglioli', - constructeur: 'bonfiglioli', - customValues: { - 'Rapport de réduction': '1:28', - 'Couple nominal (Nm)': 1600, - 'Type de montage': 'Sur arbre', - }, - }, - ], - sparePieces: [ - { - name: 'Palier UCFL207', - reference: 'SP-PALIER-160', - typeCode: 'roller-bearing', - modelCode: 'bearing-ucfl207', - requirementLabel: 'Paliers de ligne', - constructeur: 'skf', - customValues: { - Série: 'UCFL', - "Type d'étanchéité": 'ZZ', - }, - }, - { - name: 'Kit visserie vis 160', - reference: 'SP-VISS-SC2', - typeCode: 'hex-screw', - modelCode: 'screw-m10x60', - requirementLabel: 'Kit visserie caisson', - customValues: { - 'Diamètre (mm)': 10, - 'Longueur (mm)': 60, - 'Classe acier': '10.9', - }, - }, - { - name: 'Cartouche graisse 400g', - reference: 'SP-GRAISSE-02', - typeCode: 'lubrication-cartridge', - modelCode: 'grease-cartridge-400', - requirementLabel: 'Cartouches de graisse', - customValues: { - 'Volume (cm³)': 400, - 'Grade de graisse': 'NLGI 2', - }, - }, - ], - }, - { - code: 'weigh-hopper-truck', - typeMachineCode: 'weigh-hopper', - name: 'Benne peseuse camion 5T', - reference: 'BP-5000-01', - prix: '39200', - constructeurKey: 'buhler', - customFieldValues: { - 'Capacité de pesée (kg)': 5000, - 'Précision (%)': 0.5, - 'Mode de vidange': 'Trappe motorisée', - }, - components: [ - { - name: 'Cadre peseur 5T', - reference: 'BP-CADRE-01', - typeCode: 'weigh-load-frame', - requirementLabel: 'Cadre peseur', - modelCode: 'weigh-frame-5t', - constructeur: 'buhler', - customValues: { - 'Capacité nominale (kg)': 5000, - 'Nombre de capteurs': 4, - 'Protection IP': 'IP65', - }, - }, - { - name: 'Vanne hydraulique rapide', - reference: 'BP-VANNE-01', - typeCode: 'weigh-discharge-gate', - requirementLabel: 'Vanne de vidange', - modelCode: 'weigh-gate-hydraulic', - constructeur: 'poclain', - customValues: { - "Type d'actionneur": 'Hydraulique', - "Temps d'ouverture (s)": 4, - }, - }, - { - name: 'Coffret pesage camion', - reference: 'ARM-BP-01', - typeCode: 'control-panel', - requirementLabel: 'Coffret de pesage', - modelCode: 'control-panel-m340', - constructeur: 'buhler', - customValues: { - 'Automate principal': 'Schneider Modicon M340', - 'Année de mise à jour': 2023, - "Indice de protection": 'IP55', - }, - }, - ], - sparePieces: [ - { - name: 'Capteur pesage 5T', - reference: 'SP-LC-5T-01', - typeCode: 'load-cell', - modelCode: 'load-cell-5t', - requirementLabel: 'Capteurs de pesage', - constructeur: 'ifm', - customValues: { - 'Capacité (kg)': 5000, - 'Type de connexion': 'Câble 6 fils', - }, - }, - { - name: 'Capteur pesage secours', - reference: 'SP-LC-5T-02', - typeCode: 'load-cell', - modelCode: 'load-cell-5t', - requirementLabel: 'Capteurs de pesage', - constructeur: 'ifm', - customValues: { - 'Capacité (kg)': 5000, - 'Type de connexion': 'Câble 6 fils', - }, - }, - { - name: 'Fusibles 40A coffret', - reference: 'SP-FS-40A-01', - typeCode: 'fuse-cartridge', - modelCode: 'fuse-gg-40a', - requirementLabel: 'Fusibles de protection', - customValues: { - 'Calibre (A)': 40, - Type: 'gG', - }, - }, - { - name: 'Joint trappe benne', - reference: 'SP-JNT-BP', - typeCode: 'flat-gasket', - modelCode: 'gasket-ht-200', - requirementLabel: 'Joint trappe', - customValues: { - Matière: 'NBR', - 'Épaisseur (mm)': 5, - }, - }, - ], - }, - { - code: 'telehandler-mlt841', - typeMachineCode: 'telehandler', - name: 'Manitou MLT 841 Logistique', - reference: 'MLT-841-01', - prix: '87500', - constructeurKey: 'manitou', - customFieldValues: { - 'Usage principal': 'Chargement camions', - 'Lieu de stationnement': 'Hangar nord', - "Année d'achat": 2021, - }, - components: [ - { - name: 'Flèche télescopique 8 m', - reference: 'MLT-BOOM-01', - typeCode: 'telehandler-boom', - requirementLabel: 'Flèche télescopique', - modelCode: 'tele-boom-8m', - constructeur: 'manitou', - customValues: { - 'Hauteur max (m)': 8, - 'Sections télescopiques': 4, - 'Type de guidage': 'Galets', - }, - }, - { - name: 'Groupe hydraulique 140 l/min', - reference: 'MLT-HYDRO-01', - typeCode: 'hydraulic-power-pack', - requirementLabel: 'Groupe hydraulique', - modelCode: 'hydraulic-pack-tele', - constructeur: 'poclain', - customValues: { - 'Débit nominal (l/min)': 140, - 'Pression max (bar)': 280, - 'Type de pompe': 'Piston axial', - }, - }, - { - name: 'Groupe moteur 110 kW', - reference: 'MLT-MOTEUR-01', - typeCode: 'motor-drive', - requirementLabel: 'Groupe moteur', - modelCode: 'motor-drive-110', - constructeur: 'sew', - customValues: { - 'Puissance nominale (kW)': 110, - 'Classe énergétique': 'IE3', - "Indice de protection": 'IP55', - }, - }, - { - name: 'Cabine premium climatisée', - reference: 'MLT-CAB-01', - typeCode: 'telehandler-cab-module', - requirementLabel: 'Cabine opérateur', - modelCode: 'tele-cab-premium', - constructeur: 'manitou', - customValues: { - 'Type de cabine': 'Premium climatisée', - Climatisation: true, - 'Nombre de caméras': 2, - }, - }, - { - name: 'Support attache rapide', - reference: 'MLT-ATTACHE-01', - typeCode: 'telehandler-attachment-carrier', - requirementLabel: 'Support d’outils', - modelCode: 'tele-carrier-quick', - constructeur: 'manitou', - customValues: { - "Type d'attache": 'Attache Manitou', - 'Capacité nominale (t)': 4.1, - }, - }, - ], - sparePieces: [ - { - name: 'Flexible hydraulique 2 tresses', - reference: 'SP-HOSE-01', - typeCode: 'hydraulic-hose', - modelCode: 'hydraulic-hose-2w', - requirementLabel: 'Flexibles hydrauliques', - constructeur: 'poclain', - customValues: { - 'Pression max (bar)': 420, - 'Type de renfort': '2 tresses acier', - 'Longueur (mm)': 1800, - }, - }, - { - name: 'Fusibles cabine 40A', - reference: 'SP-FS-CAB', - typeCode: 'fuse-cartridge', - modelCode: 'fuse-gg-40a', - requirementLabel: 'Fusibles cabine', - customValues: { - 'Calibre (A)': 40, - Type: 'gG', - }, - }, - { - name: 'Cartouche graisse MLT', - reference: 'SP-GRAISSE-MLT', - typeCode: 'lubrication-cartridge', - modelCode: 'grease-cartridge-400', - requirementLabel: 'Cartouches graissage', - customValues: { - 'Volume (cm³)': 400, - 'Grade de graisse': 'NLGI 2', - }, - }, - ], - }, -]; - -async function clearDatabaseExceptSitesAndProfiles() { - console.log('🧹 Nettoyage des tables (hors sites et profils)...'); - - const deleteOrder = [ - prisma.customFieldValue.deleteMany(), - prisma.document.deleteMany(), - prisma.piece.deleteMany(), - prisma.composant.deleteMany(), - prisma.machine.deleteMany(), - prisma.typeMachineComponentRequirement.deleteMany(), - prisma.typeMachinePieceRequirement.deleteMany(), - prisma.customField.deleteMany(), - prisma.typeMachine.deleteMany(), - prisma.modelType.deleteMany(), - prisma.constructeur.deleteMany(), - ]; - - for (const promise of deleteOrder) { - await promise; - } - - console.log('✅ Tables nettoyées.'); -} - -async function ensureDemoSite() { - const existingSite = await prisma.site.findFirst(); - if (existingSite) { - return existingSite; - } - - console.log('🏗️ Création du site de démonstration...'); - return prisma.site.create({ - data: { - name: 'Usine de triage Valgrain', - contactName: 'Lucie Bernard', - contactPhone: '+33 3 80 12 45 78', - contactAddress: 'Zone industrielle des Platanes', - contactPostalCode: '21000', - contactCity: 'Dijon', - }, - }); -} - -async function createConstructeurs() { - console.log('🏭 Création des constructeurs...'); - const entries = await Promise.all( - constructeurDefinitions.map((definition) => - prisma.constructeur - .create({ - data: { - name: definition.name, - email: definition.email, - phone: definition.phone, - }, - }) - .then((constructeur) => [definition.key, constructeur] as const), - ), - ); - - return Object.fromEntries(entries) as Record; -} - -function mapCustomFields(fields: { id: string; name: string }[]) { - return fields.reduce>((acc, field) => { - acc[field.name] = field.id; - return acc; - }, {}); -} - -async function createModelTypes() { - console.log('🗂️ Création des catégories de composants et pièces...'); - - const componentTypeEntries: [ - string, - { id: string; customFields: Record }, - ][] = []; - - for (const definition of componentTypeDefinitions) { - const record = await prisma.modelType.create({ - data: { - name: definition.name, - code: definition.code, - category: ModelCategory.COMPONENT, - description: definition.description, - customFields: { - create: definition.customFields.map((field) => ({ - name: field.name, - type: field.type, - required: field.required ?? false, - defaultValue: field.defaultValue, - options: field.options ?? [], - })), - }, - }, - include: { customFields: true }, - }); - - componentTypeEntries.push([ - definition.code, - { id: record.id, customFields: mapCustomFields(record.customFields) }, - ]); - } - - const pieceTypeEntries: [ - string, - { id: string; customFields: Record }, - ][] = []; - - for (const definition of pieceTypeDefinitions) { - const record = await prisma.modelType.create({ - data: { - name: definition.name, - code: definition.code, - category: ModelCategory.PIECE, - description: definition.description, - customFields: { - create: definition.customFields.map((field) => ({ - name: field.name, - type: field.type, - required: field.required ?? false, - defaultValue: field.defaultValue, - options: field.options ?? [], - })), - }, - }, - include: { customFields: true }, - }); - - pieceTypeEntries.push([ - definition.code, - { id: record.id, customFields: mapCustomFields(record.customFields) }, - ]); - } - - const componentTypesMap = Object.fromEntries(componentTypeEntries) as Record< - string, - { id: string; customFields: Record } - >; - const pieceTypesMap = Object.fromEntries(pieceTypeEntries) as Record< - string, - { id: string; customFields: Record } - >; - - await applyPieceTypeSkeletons(pieceTypesMap); - await applyComponentTypeSkeletons(componentTypesMap, pieceTypesMap); - - return { - componentTypes: componentTypesMap, - pieceTypes: pieceTypesMap, - }; -} - -async function applyPieceTypeSkeletons( - pieceTypes: Record, -) { - console.log('🧩 Application des squelettes de pièces...'); - - const applied = new Set(); - - for (const definition of pieceModelDefinitions) { - const type = pieceTypes[definition.typeCode]; - if (!type || !definition.structure || applied.has(type.id)) { - continue; - } - - try { - const skeleton = PieceModelStructureSchema.parse(definition.structure); - await prisma.modelType.update({ - where: { id: type.id }, - data: { pieceSkeleton: skeleton as Prisma.InputJsonValue }, - }); - applied.add(type.id); - } catch (error) { - console.warn( - `⚠️ Impossible d'appliquer le squelette de pièce ${definition.code}:`, - error, - ); - } - } -} - -function buildComponentModelStructure( - structure: ComponentModelStructureDraft | Prisma.InputJsonValue | undefined, - componentTypes: Record, - pieceTypes: Record, -): Prisma.InputJsonValue | undefined { - if (!structure) { - return undefined; - } - - if (typeof structure !== 'object' || structure === null) { - return structure as Prisma.InputJsonValue; - } - - const candidate = structure as Record; - - const asCleanString = (value: unknown): string | undefined => { - if (value === undefined || value === null) { - return undefined; - } - const text = String(value).trim().replace(/\s+/g, ' '); - return text ? text : undefined; - }; - - if ( - Array.isArray((candidate as any).pieces) || - Array.isArray((candidate as any).customFields) || - Array.isArray((candidate as any).subcomponents) || - Array.isArray((candidate as any).subComponents) - ) { - return normalizeComponentModelStructure(candidate) as Prisma.InputJsonValue; - } - - const customFieldEntries = new Map(); - - const presetCustomFields = Array.isArray((candidate as any).customFields) - ? ((candidate as any).customFields as Array>) - : []; - - for (const entry of presetCustomFields) { - const key = String(entry?.key ?? entry?.name ?? '').trim(); - if (!key) { - continue; - } - if (!customFieldEntries.has(key)) { - customFieldEntries.set(key, entry?.value ?? null); - } - } - - const recommended = (candidate as any).recommendedCustomFields as - | Record - | undefined; - - if (recommended) { - for (const [key, value] of Object.entries(recommended)) { - const trimmedKey = String(key).trim(); - if (!trimmedKey) { - continue; - } - if (!customFieldEntries.has(trimmedKey)) { - customFieldEntries.set(trimmedKey, value ?? null); - } - } - } - - const pieces: ComponentModelStructure['pieces'] = []; - const subcomponents: ComponentModelStructure['subcomponents'] = []; - - const pieceTemplates = Array.isArray((candidate as any).pieceTemplates) - ? ((candidate as any).pieceTemplates as Array>) - : []; - - for (const template of pieceTemplates) { - const typeCode = String(template?.typeCode ?? '').trim(); - if (!typeCode) { - continue; - } - - const normalizedRole = asCleanString( - template?.usage ?? template?.role ?? template?.notes ?? null, - ); - - const pieceType = pieceTypes[typeCode]; - if (pieceType) { - const entry: Record = { typePieceId: pieceType.id }; - if (normalizedRole) { - entry.role = normalizedRole; - } - entry.familyCode = typeCode; - pieces.push(entry as ComponentModelStructure['pieces'][number]); - continue; - } - - const subcomponentType = componentTypes[typeCode]; - if (subcomponentType) { - const entry: Record = { - typeComposantId: subcomponentType.id, - familyCode: typeCode, - }; - if (normalizedRole) { - entry.alias = normalizedRole; - } - subcomponents.push(entry as ComponentModelStructure['subcomponents'][number]); - continue; - } - - pieces.push( - normalizedRole - ? { familyCode: typeCode, role: normalizedRole } - : { familyCode: typeCode }, - ); - } - - const subComponentTemplates = Array.isArray((candidate as any).subComponentTemplates) - ? ((candidate as any).subComponentTemplates as Array>) - : []; - - for (const template of subComponentTemplates) { - const typeCode = String(template?.typeCode ?? '').trim(); - const normalizedAlias = asCleanString(template?.alias ?? template?.notes ?? null); - const suggestedModels = Array.isArray(template?.suggestedModelCodes) - ? (template?.suggestedModelCodes as string[]) - : []; - - const componentType = typeCode ? componentTypes[typeCode] : undefined; - - if (suggestedModels.length > 0) { - suggestedModels.forEach((modelCode, index) => { - const hint = - normalizedAlias && suggestedModels.length > 1 - ? `${normalizedAlias} #${index + 1}` - : normalizedAlias; - const fallbackFamily = typeCode || String(modelCode ?? '').trim() || 'UNKNOWN'; - const entry: Record = hint ? { alias: hint } : {}; - - if (componentType) { - entry.typeComposantId = componentType.id; - entry.familyCode = typeCode; - } else { - entry.familyCode = fallbackFamily; - } - - subcomponents.push(entry as ComponentModelStructure['subcomponents'][number]); - }); - continue; - } - - if (typeCode) { - if (componentType) { - const entry: Record = { - typeComposantId: componentType.id, - familyCode: typeCode, - }; - if (normalizedAlias) { - entry.alias = normalizedAlias; - } - subcomponents.push(entry as ComponentModelStructure['subcomponents'][number]); - } else { - subcomponents.push( - normalizedAlias - ? { familyCode: typeCode, alias: normalizedAlias } - : { familyCode: typeCode }, - ); - } - continue; - } - } - - const canonical: ComponentModelStructure = { - pieces, - subcomponents, - customFields: Array.from(customFieldEntries.entries()).map(([key, value]) => ({ - key, - value: value ?? null, - })), - }; - - return normalizeComponentModelStructure(canonical) as Prisma.InputJsonValue; -} - -async function applyComponentTypeSkeletons( - componentTypes: Record, - pieceTypes: Record, -) { - console.log('🛠️ Application des squelettes de composants...'); - - const applied = new Set(); - - for (const definition of componentModelDefinitions) { - const type = componentTypes[definition.typeCode]; - if (!type || applied.has(type.id)) { - continue; - } - - const structure = buildComponentModelStructure( - definition.structure, - componentTypes, - pieceTypes, - ); - - if (!structure) { - continue; - } - - try { - const skeleton = ComponentModelStructureSchema.parse(structure); - await prisma.modelType.update({ - where: { id: type.id }, - data: { componentSkeleton: skeleton as Prisma.InputJsonValue }, - }); - applied.add(type.id); - } catch (error) { - console.warn( - `⚠️ Impossible d'appliquer le squelette de composant ${definition.code}:`, - error, - ); - } - } -} - -async function createTypeMachines( - componentTypes: Record, - pieceTypes: Record, -) { - console.log('🧬 Création des squelettes de machines...'); - - const entries = await Promise.all( - typeMachineDefinitions.map(async (definition) => { - const record = await prisma.typeMachine.create({ - data: { - name: definition.name, - description: definition.description, - category: definition.category, - maintenanceFrequency: definition.maintenanceFrequency, - specifications: definition.specifications, - components: { - layout: definition.componentRequirements.map((requirement, index) => ({ - order: index + 1, - zone: requirement.label, - type: requirement.typeCode, - })), - }, - machinePieces: { - recommendedStock: [], - }, - customFields: { - create: definition.customFields.map((field) => ({ - name: field.name, - type: field.type, - required: field.required ?? false, - defaultValue: field.defaultValue, - options: field.options ?? [], - })), - }, - componentRequirements: { - create: definition.componentRequirements.map((requirement) => ({ - label: requirement.label, - minCount: requirement.minCount, - maxCount: requirement.maxCount, - required: requirement.required, - typeComposant: { - connect: { id: componentTypes[requirement.typeCode].id }, - }, - })), - }, - pieceRequirements: definition.pieceRequirements - ? { - create: definition.pieceRequirements.map((requirement) => ({ - label: requirement.label, - minCount: requirement.minCount, - maxCount: requirement.maxCount, - required: requirement.required, - typePiece: { - connect: { id: pieceTypes[requirement.typeCode].id }, - }, - })), - } - : undefined, - }, - include: { - customFields: true, - componentRequirements: true, - pieceRequirements: true, - }, - }); - - return [definition.code, record] as const; - }), - ); - - return Object.fromEntries(entries) as Record; -} - -function buildCustomFieldValues( - fieldMap: Record, - values?: Record, -) { - if (!values) { - return undefined; - } - - return { - create: Object.entries(values).map(([name, value]) => { - const fieldId = fieldMap[name]; - if (!fieldId) { - throw new Error(`Champ personnalisé inconnu: ${name}`); - } - return { - value: value instanceof Date ? value.toISOString() : String(value), - customField: { - connect: { id: fieldId }, - }, - }; - }), - }; -} - -async function createComponentHierarchy( - machineId: string, - component: ComponentInstance, - context: { - componentTypes: Record }>; - pieceTypes: Record }>; - constructeurs: Record; - requirementMap: Map; - }, - parentId?: string, -) { - const requirementId = component.requirementLabel - ? context.requirementMap.get(component.requirementLabel) - : undefined; - - const record = await prisma.composant.create({ - data: { - name: component.name, - reference: component.reference, - prix: component.prix ? new Prisma.Decimal(component.prix) : undefined, - machine: { connect: { id: machineId } }, - parentComposant: parentId ? { connect: { id: parentId } } : undefined, - typeComposant: { connect: { id: context.componentTypes[component.typeCode].id } }, - constructeur: component.constructeur - ? { connect: { id: context.constructeurs[component.constructeur].id } } - : undefined, - typeMachineComponentRequirement: requirementId - ? { connect: { id: requirementId } } - : undefined, - customFieldValues: buildCustomFieldValues( - context.componentTypes[component.typeCode].customFields, - component.customValues, - ), - pieces: component.pieces - ? { - create: component.pieces.map((piece) => { - const type = context.pieceTypes[piece.typeCode]; - if (!type) { - throw new Error(`Type de pièce introuvable: ${piece.typeCode}`); - } - return { - name: piece.name, - reference: piece.reference, - prix: piece.prix ? new Prisma.Decimal(piece.prix) : undefined, - typePiece: { connect: { id: type.id } }, - constructeur: piece.constructeur - ? { connect: { id: context.constructeurs[piece.constructeur].id } } - : undefined, - customFieldValues: buildCustomFieldValues( - type.customFields, - piece.customValues, - ), - }; - }), - } - : undefined, - }, - }); - - if (component.children && component.children.length > 0) { - for (const child of component.children) { - await createComponentHierarchy(machineId, child, context, record.id); - } - } - - return record; -} - -async function createMachines( - siteId: string, - typeMachines: Record, - context: { - componentTypes: Record }>; - pieceTypes: Record }>; - constructeurs: Record; - }, -) { - console.log('🏗️ Création des machines de démonstration...'); - - for (const build of machineBuilds) { - const typeMachine = typeMachines[build.typeMachineCode]; - if (!typeMachine) { - throw new Error(`Type de machine introuvable pour ${build.typeMachineCode}`); - } - - const requirementMap = new Map(); - typeMachine.componentRequirements.forEach((requirement) => { - if (requirement.label) { - requirementMap.set(requirement.label, requirement.id); - } - }); - - const pieceRequirementMap = new Map(); - typeMachine.pieceRequirements.forEach((requirement) => { - if (requirement.label) { - pieceRequirementMap.set(requirement.label, requirement.id); - } - }); - - const machineCustomFieldMap = mapCustomFields(typeMachine.customFields); - - const machine = await prisma.machine.create({ - data: { - name: build.name, - reference: build.reference, - prix: new Prisma.Decimal(build.prix), - site: { connect: { id: siteId } }, - typeMachine: { connect: { id: typeMachine.id } }, - constructeur: { - connect: { id: context.constructeurs[build.constructeurKey].id }, - }, - customFieldValues: buildCustomFieldValues( - machineCustomFieldMap, - build.customFieldValues, - ), - }, - }); - - console.log(`⚙️ ${build.name} - ajout des composants...`); - - const componentContext = { - ...context, - requirementMap, - }; - - for (const component of build.components) { - await createComponentHierarchy(machine.id, component, componentContext); - } - - if (build.sparePieces && build.sparePieces.length > 0) { - console.log(`📦 ${build.name} - enregistrement des pièces de réserve...`); - for (const spare of build.sparePieces) { - const pieceType = context.pieceTypes[spare.typeCode]; - if (!pieceType) { - throw new Error(`Type de pièce inconnu pour pièce de secours: ${spare.typeCode}`); - } - const requirementId = spare.requirementLabel - ? pieceRequirementMap.get(spare.requirementLabel) - : undefined; - - await prisma.piece.create({ - data: { - name: spare.name, - reference: spare.reference, - prix: spare.prix ? new Prisma.Decimal(spare.prix) : undefined, - machine: { connect: { id: machine.id } }, - typePiece: { connect: { id: pieceType.id } }, - constructeur: spare.constructeur - ? { connect: { id: context.constructeurs[spare.constructeur].id } } - : undefined, - typeMachinePieceRequirement: requirementId - ? { connect: { id: requirementId } } - : undefined, - customFieldValues: buildCustomFieldValues( - pieceType.customFields, - spare.customValues, - ), - }, - }); - } - } - } -} - -async function main() { - try { - await clearDatabaseExceptSitesAndProfiles(); - - const [site, constructeurs] = await Promise.all([ - ensureDemoSite(), - createConstructeurs(), - ]); - - const { componentTypes, pieceTypes } = await createModelTypes(); - const typeMachines = await createTypeMachines(componentTypes, pieceTypes); - - await createMachines(site.id, typeMachines, { - componentTypes, - pieceTypes, - constructeurs, - }); - - console.log('🎉 Données de démonstration générées avec succès.'); - } catch (error) { - console.error('❌ Erreur pendant la génération des données :', error); - process.exitCode = 1; - } finally { - await prisma.$disconnect(); - } -} - -main(); diff --git a/scripts/seed-sample-data.ts b/scripts/seed-sample-data.ts new file mode 100644 index 0000000..1d59d84 --- /dev/null +++ b/scripts/seed-sample-data.ts @@ -0,0 +1,550 @@ +import { PrismaClient, Prisma } from '@prisma/client'; + +const prisma = new PrismaClient(); + +type CreatedFields = Record; + +async function deleteExistingData() { + await prisma.machineComponentLink.deleteMany(); + await prisma.machinePieceLink.deleteMany(); + await prisma.machine.deleteMany(); + await prisma.customFieldValue.deleteMany(); + await prisma.composant.deleteMany(); + await prisma.piece.deleteMany(); + + await prisma.modelType.deleteMany({ + where: { + code: { + in: [ + 'hydraulic-pump', + 'hydraulic-reservoir', + 'cooling-fan', + 'cooling-module', + 'structural-frame', + 'hydraulic-power-unit', + ], + }, + }, + }); +} + +async function createPieceType( + name: string, + code: string, + description: string, + fields: Array<{ + name: string; + type: 'text' | 'number' | 'select' | 'boolean' | 'date'; + required?: boolean; + options?: string[]; + }>, + skeleton?: Record, +) { + const type = await prisma.modelType.create({ + data: { + name, + code, + category: 'PIECE', + description, + pieceSkeleton: skeleton + ? (skeleton as Prisma.InputJsonValue) + : Prisma.JsonNull, + pieceCustomFields: { + create: fields.map((field) => ({ + name: field.name, + type: field.type, + required: field.required ?? false, + options: field.options ?? [], + })), + }, + }, + }); + + const customFields = await prisma.customField.findMany({ + where: { typePieceId: type.id }, + }); + + const fieldMap: CreatedFields = {}; + customFields.forEach((field) => { + fieldMap[field.name] = field.id; + }); + + return { type, fieldMap }; +} + +async function createComponentType( + name: string, + code: string, + description: string, + fields: Array<{ + name: string; + type: 'text' | 'number' | 'select' | 'boolean' | 'date'; + required?: boolean; + options?: string[]; + }>, + skeleton?: Record, +) { + const type = await prisma.modelType.create({ + data: { + name, + code, + category: 'COMPONENT', + description, + componentSkeleton: skeleton + ? (skeleton as Prisma.InputJsonValue) + : Prisma.JsonNull, + customFields: { + create: fields.map((field) => ({ + name: field.name, + type: field.type, + required: field.required ?? false, + options: field.options ?? [], + })), + }, + }, + }); + + const customFields = await prisma.customField.findMany({ + where: { typeComposantId: type.id }, + }); + + const fieldMap: CreatedFields = {}; + customFields.forEach((field) => { + fieldMap[field.name] = field.id; + }); + + return { type, fieldMap }; +} + +async function createPiece(options: { + name: string; + reference: string; + price: number; + constructeurId?: string | null; + typeId: string; + fieldValues: Record; +}) { + const customFields = await prisma.customField.findMany({ + where: { typePieceId: options.typeId }, + }); + + const customFieldValues = Object.entries(options.fieldValues).map( + ([fieldName, value]) => { + const target = customFields.find((field) => field.name === fieldName); + if (!target) { + throw new Error( + `Custom field "${fieldName}" introuvable pour le type de pièce ${options.typeId}`, + ); + } + return { + value, + customFieldId: target.id, + }; + }, + ); + + return prisma.piece.create({ + data: { + name: options.name, + reference: options.reference, + prix: new Prisma.Decimal(options.price), + typePieceId: options.typeId, + constructeurId: options.constructeurId ?? null, + customFieldValues: { + create: customFieldValues, + }, + }, + }); +} + +async function createComponent(options: { + name: string; + reference: string; + price: number; + constructeurId?: string | null; + typeId: string; + fieldValues: Record; + structure?: Prisma.InputJsonValue; +}) { + const customFields = await prisma.customField.findMany({ + where: { typeComposantId: options.typeId }, + }); + + const customFieldValues = Object.entries(options.fieldValues).map( + ([fieldName, value]) => { + const target = customFields.find((field) => field.name === fieldName); + if (!target) { + throw new Error( + `Custom field "${fieldName}" introuvable pour le type de composant ${options.typeId}`, + ); + } + return { + value, + customFieldId: target.id, + }; + }, + ); + + return prisma.composant.create({ + data: { + name: options.name, + reference: options.reference, + prix: new Prisma.Decimal(options.price), + typeComposantId: options.typeId, + constructeurId: options.constructeurId ?? null, + structure: + options.structure === undefined + ? Prisma.JsonNull + : options.structure ?? Prisma.JsonNull, + customFieldValues: { + create: customFieldValues, + }, + }, + }); +} + +async function main() { + console.log('Nettoyage des données existantes…'); + await deleteExistingData(); + + console.log('Création des types de pièces…'); + const pumpPieceFields: { + name: string; + type: 'text' | 'number' | 'select' | 'boolean' | 'date'; + required?: boolean; + options?: string[]; + }[] = [ + { name: 'Pression nominale (bar)', type: 'number', required: true }, + { name: 'Débit nominal (L/min)', type: 'number', required: true }, + { name: 'Puissance (kW)', type: 'number', required: true }, + { name: 'Indice de protection', type: 'text' }, + { name: 'Vitesse max (rpm)', type: 'number' }, + ] as const; + + const pumpPieceType = await createPieceType( + 'Pompe hydraulique', + 'hydraulic-pump', + 'Pompes à pistons pour unités hydrauliques', + pumpPieceFields, + { + customFields: pumpPieceFields.map((field) => ({ + name: field.name, + key: field.name, + type: field.type, + required: field.required ?? false, + })), + }, + ); + + const reservoirPieceFields: { + name: string; + type: 'text' | 'number' | 'select' | 'boolean' | 'date'; + required?: boolean; + options?: string[]; + }[] = [ + { name: 'Pression nominale (bar)', type: 'number', required: true }, + { name: 'Débit nominal (L/min)', type: 'number', required: true }, + { name: 'Capacité (L)', type: 'number', required: true }, + { name: 'Type d’huile', type: 'text', required: true }, + { name: 'Filtration', type: 'text' }, + ] as const; + + const reservoirPieceType = await createPieceType( + 'Réservoir hydraulique', + 'hydraulic-reservoir', + 'Réservoirs haute capacité pour fluide hydraulique', + reservoirPieceFields, + { + customFields: reservoirPieceFields.map((field) => ({ + name: field.name, + key: field.name, + type: field.type, + required: field.required ?? false, + })), + }, + ); + + const fanPieceFields: { + name: string; + type: 'text' | 'number' | 'select' | 'boolean' | 'date'; + required?: boolean; + options?: string[]; + }[] = [ + { name: 'Diamètre (mm)', type: 'number', required: true }, + { name: 'Débit air (m³/h)', type: 'number', required: true }, + { name: 'Consommation (A)', type: 'number' }, + { name: 'Tension (V)', type: 'number', required: true }, + { name: 'Niveau sonore (dB)', type: 'number' }, + ] as const; + + const fanPieceType = await createPieceType( + 'Ventilateur de refroidissement', + 'cooling-fan', + 'Ventilateurs axiaux pour modules de refroidissement', + fanPieceFields, + { + customFields: fanPieceFields.map((field) => ({ + name: field.name, + key: field.name, + type: field.type, + required: field.required ?? false, + })), + }, + ); + + console.log('Création des pièces…'); + const pumpPiece = await createPiece({ + name: 'Pompe à pistons PX-300', + reference: 'PX-300', + price: 1850, + typeId: pumpPieceType.type.id, + fieldValues: { + 'Pression nominale (bar)': '180', + 'Débit nominal (L/min)': '260', + 'Puissance (kW)': '45', + 'Indice de protection': 'IP55', + 'Vitesse max (rpm)': '3200', + }, + }); + + const reservoirPiece = await createPiece({ + name: 'Réservoir 120L Inox', + reference: 'RES-120-INX', + price: 720, + typeId: reservoirPieceType.type.id, + fieldValues: { + 'Pression nominale (bar)': '12', + 'Débit nominal (L/min)': '280', + 'Capacité (L)': '120', + 'Type d’huile': 'HLP46', + Filtration: '10µ absolu', + }, + }); + + const fanPiece = await createPiece({ + name: 'Ventilateur axial VE-450', + reference: 'VE-450', + price: 390, + typeId: fanPieceType.type.id, + fieldValues: { + 'Diamètre (mm)': '450', + 'Débit air (m³/h)': '5200', + 'Consommation (A)': '3.2', + 'Tension (V)': '400', + 'Niveau sonore (dB)': '68', + }, + }); + + console.log('Création des types de composants…'); + const coolingComponentFields: { + name: string; + type: 'text' | 'number' | 'select' | 'boolean' | 'date'; + required?: boolean; + options?: string[]; + }[] = [ + { name: 'Type de fluide', type: 'text', required: true }, + { name: 'Puissance thermique (kW)', type: 'number', required: true }, + { name: 'Température max (°C)', type: 'number' }, + { name: 'Débit eau (L/min)', type: 'number' }, + ]; + + const coolingComponentType = await createComponentType( + 'Module de refroidissement', + 'cooling-module', + 'Modules compacts pour dissipation thermique du circuit hydraulique', + coolingComponentFields, + { + customFields: coolingComponentFields.map((field) => ({ + name: field.name, + key: field.name, + type: field.type, + required: field.required ?? false, + })), + pieces: [ + { + typePieceId: fanPieceType.type.id, + role: 'Ventilation principale', + }, + ], + subcomponents: [], + }, + ); + + const frameComponentFields: { + name: string; + type: 'text' | 'number' | 'select' | 'boolean' | 'date'; + required?: boolean; + options?: string[]; + }[] = [ + { name: 'Matière', type: 'text', required: true }, + { name: 'Charge admissible (kg)', type: 'number', required: true }, + { name: 'Revêtement', type: 'text' }, + { name: 'Points de levage', type: 'number' }, + ]; + + const frameComponentType = await createComponentType( + 'Châssis structurel', + 'structural-frame', + 'Châssis mécano-soudé pour unités hydrauliques', + frameComponentFields, + { + customFields: frameComponentFields.map((field) => ({ + name: field.name, + key: field.name, + type: field.type, + required: field.required ?? false, + })), + pieces: [], + subcomponents: [], + }, + ); + + const powerUnitComponentFields: { + name: string; + type: 'text' | 'number' | 'select' | 'boolean' | 'date'; + required?: boolean; + options?: string[]; + }[] = [ + { name: 'Pression service (bar)', type: 'number', required: true }, + { name: 'Débit maxi (L/min)', type: 'number', required: true }, + { name: 'Niveau sonore (dB)', type: 'number' }, + { name: 'Indice de protection', type: 'text' }, + ]; + + const powerUnitComponentType = await createComponentType( + 'Centrale hydraulique', + 'hydraulic-power-unit', + 'Unités hydrauliques complètes pour machines industrielles', + powerUnitComponentFields, + { + customFields: powerUnitComponentFields.map((field) => ({ + name: field.name, + key: field.name, + type: field.type, + required: field.required ?? false, + })), + pieces: [ + { + typePieceId: pumpPieceType.type.id, + role: 'Pompe principale', + }, + { + typePieceId: reservoirPieceType.type.id, + role: 'Réservoir d’huile', + }, + ], + subcomponents: [ + { + typeComposantId: coolingComponentType.type.id, + alias: 'Module de refroidissement', + }, + { + typeComposantId: frameComponentType.type.id, + alias: 'Châssis structurel', + }, + ], + }, + ); + + console.log('Création des composants (sous-ensembles)…'); + const coolingModule = await createComponent({ + name: 'Module de refroidissement AquaCool 50', + reference: 'AC-50', + price: 2450, + typeId: coolingComponentType.type.id, + fieldValues: { + 'Type de fluide': 'Huile minérale', + 'Puissance thermique (kW)': '35', + 'Température max (°C)': '85', + 'Débit eau (L/min)': '65', + }, + structure: { + path: 'root', + pieces: [ + { + path: 'root:piece-0', + definition: { + typePieceId: fanPieceType.type.id, + }, + selectedPieceId: fanPiece.id, + }, + ], + subcomponents: [], + } as Prisma.InputJsonValue, + }); + + const structuralFrame = await createComponent({ + name: 'Châssis structurel XC-800', + reference: 'FRAME-XC800', + price: 1280, + typeId: frameComponentType.type.id, + fieldValues: { + Matière: 'Acier S355', + 'Charge admissible (kg)': '1800', + Revêtement: 'Peinture epoxy', + 'Points de levage': '4', + }, + }); + + console.log('Création du composant principal…'); + await createComponent({ + name: 'Centrale hydraulique HX-500', + reference: 'HX-500', + price: 12900, + typeId: powerUnitComponentType.type.id, + fieldValues: { + 'Pression service (bar)': '210', + 'Débit maxi (L/min)': '320', + 'Niveau sonore (dB)': '72', + 'Indice de protection': 'IP54', + }, + structure: { + path: 'root', + pieces: [ + { + path: 'root:piece-0', + definition: { + typePieceId: pumpPieceType.type.id, + }, + selectedPieceId: pumpPiece.id, + }, + { + path: 'root:piece-1', + definition: { + typePieceId: reservoirPieceType.type.id, + }, + selectedPieceId: reservoirPiece.id, + }, + ], + subcomponents: [ + { + path: 'root:sub-0', + definition: { + alias: 'Module de refroidissement', + typeComposantId: coolingComponentType.type.id, + }, + selectedComponentId: coolingModule.id, + }, + { + path: 'root:sub-1', + definition: { + alias: 'Châssis structurel', + typeComposantId: frameComponentType.type.id, + }, + selectedComponentId: structuralFrame.id, + }, + ], + } as Prisma.InputJsonValue, + }); + + console.log('Population terminée ✅'); +} + +main() + .catch((error) => { + console.error(error); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/src/common/constants/component-includes.ts b/src/common/constants/component-includes.ts index 771d554..537bbcb 100644 --- a/src/common/constants/component-includes.ts +++ b/src/common/constants/component-includes.ts @@ -9,46 +9,22 @@ const CUSTOM_FIELD_SELECT = { } as const; export const COMPONENT_WITH_RELATIONS_INCLUDE = { - machine: true, - parentComposant: true, typeComposant: { include: { customFields: true, }, }, - typeMachineComponentRequirement: { - include: { - typeComposant: { - include: { - customFields: true, - }, - }, - }, - }, constructeur: true, customFieldValues: { include: { customField: { select: CUSTOM_FIELD_SELECT }, }, }, - pieces: { + machineLinks: { include: { - customFieldValues: { - include: { - customField: { select: CUSTOM_FIELD_SELECT }, - }, - }, - constructeur: true, - typeMachinePieceRequirement: { - include: { - typePiece: { - include: { - customFields: true, - }, - }, - }, - }, - documents: true, + machine: true, + typeMachineComponentRequirement: true, + childLinks: true, }, }, documents: true, diff --git a/src/composants/composants.controller.ts b/src/composants/composants.controller.ts index bce234b..f33b336 100644 --- a/src/composants/composants.controller.ts +++ b/src/composants/composants.controller.ts @@ -27,16 +27,6 @@ export class ComposantsController { return this.composantsService.findAll(); } - @Get('hierarchy/:machineId') - findHierarchy(@Param('machineId') machineId: string) { - return this.composantsService.findHierarchy(machineId); - } - - @Get('machine/:machineId') - findByMachine(@Param('machineId') machineId: string) { - return this.composantsService.findByMachine(machineId); - } - @Get(':id') findOne(@Param('id') id: string) { return this.composantsService.findOne(id); diff --git a/src/composants/composants.service.spec.ts b/src/composants/composants.service.spec.ts index 587b86b..15e1d1b 100644 --- a/src/composants/composants.service.spec.ts +++ b/src/composants/composants.service.spec.ts @@ -1,32 +1,20 @@ -import { BadRequestException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ComposantsService } from './composants.service'; import { PrismaService } from '../prisma/prisma.service'; -import { CreateComposantDto } from '../shared/dto/composant.dto'; +import { CreateComposantDto, UpdateComposantDto } from '../shared/dto/composant.dto'; describe('ComposantsService', () => { let service: ComposantsService; - let prisma: any; + let prisma: { composant: any }; beforeEach(async () => { prisma = { composant: { create: jest.fn(), + findMany: jest.fn(), findUnique: jest.fn(), - findMany: jest.fn(), - }, - machine: { - findUnique: jest.fn(), - }, - customField: { - findMany: jest.fn(), - }, - customFieldValue: { - findMany: jest.fn(), - create: jest.fn(), - }, - piece: { - create: jest.fn(), + update: jest.fn(), + delete: jest.fn(), }, }; @@ -40,248 +28,27 @@ describe('ComposantsService', () => { service = module.get(ComposantsService); }); - afterEach(() => { - jest.clearAllMocks(); - }); - - it('should create a component when requirement matches the machine skeleton', async () => { + it('creates a component', async () => { const dto: CreateComposantDto = { name: 'Comp A', - machineId: 'machine-1', - typeComposantId: 'type-comp-1', - typeMachineComponentRequirementId: 'req-1', + typeComposantId: 'type-1', }; - prisma.machine.findUnique.mockResolvedValue({ - id: 'machine-1', - typeMachine: { - componentRequirements: [ - { - id: 'req-1', - typeComposantId: 'type-comp-1', - typeComposant: { - id: 'type-comp-1', - name: 'Comp type', - code: 'comp-type', - componentSkeleton: null, - }, - }, - ], - pieceRequirements: [], - }, - }); + prisma.composant.create.mockResolvedValue({ id: 'comp-1', name: dto.name }); - const created = { - id: 'component-1', - name: 'Comp A', - machineId: 'machine-1', - typeComposantId: 'type-comp-1', - }; - prisma.composant.create.mockResolvedValue(created); - prisma.composant.findUnique.mockResolvedValue({ - ...created, - machine: null, - parentComposant: null, - typeComposant: { - id: 'type-comp-1', - name: 'Comp type', - code: 'comp-type', - componentSkeleton: null, - customFields: [], - }, - typeMachineComponentRequirement: null, - constructeur: null, - customFieldValues: [], - pieces: [], - documents: [], - }); - prisma.composant.findMany.mockResolvedValue([]); - - await expect(service.create(dto)).resolves.toMatchObject({ - id: 'component-1', - }); + const result = await service.create(dto); expect(prisma.composant.create).toHaveBeenCalled(); - expect(prisma.composant.create.mock.calls[0][0].data.typeComposantId).toBe( - 'type-comp-1', - ); + expect(result).toMatchObject({ id: 'comp-1' }); }); - it('should refuse creation when requirement does not belong to machine skeleton', async () => { - const dto: CreateComposantDto = { - name: 'Comp A', - machineId: 'machine-1', - typeComposantId: 'type-comp-1', - typeMachineComponentRequirementId: 'req-2', - }; + it('updates a component', async () => { + const dto: UpdateComposantDto = { name: 'Updated' }; - prisma.machine.findUnique.mockResolvedValue({ - id: 'machine-1', - typeMachine: { - componentRequirements: [ - { id: 'req-1', typeComposantId: 'type-comp-1' }, - ], - pieceRequirements: [], - }, - }); + prisma.composant.update.mockResolvedValue({ id: 'comp-1', name: 'Updated' }); - await expect(service.create(dto)).rejects.toBeInstanceOf( - BadRequestException, - ); + await service.update('comp-1', dto); - expect(prisma.composant.create).not.toHaveBeenCalled(); - }); - - it('should create nested components, pieces, and custom field values from the type skeleton', async () => { - const dto: CreateComposantDto = { - name: 'Comp B', - machineId: 'machine-1', - typeMachineComponentRequirementId: 'req-root', - } as any; - - prisma.machine.findUnique.mockResolvedValue({ - id: 'machine-1', - typeMachine: { - componentRequirements: [ - { - id: 'req-root', - typeComposantId: 'type-root', - typeComposant: { - id: 'type-root', - name: 'Root type', - code: 'root', - componentSkeleton: { - customFields: [{ key: 'color', value: 'red' }], - pieces: [ - { - typePieceId: 'type-piece', - role: 'Primary piece', - }, - ], - subcomponents: [ - { - typeComposantId: 'type-child', - alias: 'Child component', - }, - ], - }, - }, - maxCount: null, - }, - { - id: 'req-child', - typeComposantId: 'type-child', - typeComposant: { - id: 'type-child', - name: 'Child type', - code: 'child', - componentSkeleton: null, - }, - maxCount: null, - }, - ], - pieceRequirements: [ - { - id: 'req-piece', - typePieceId: 'type-piece', - typePiece: { - id: 'type-piece', - name: 'Piece type', - code: 'piece', - }, - maxCount: null, - }, - ], - }, - }); - - prisma.customField.findMany.mockResolvedValue([ - { id: 'cf-color', name: 'color' }, - ]); - prisma.customFieldValue.findMany.mockResolvedValue([]); - - const rootComponent = { - id: 'component-1', - name: 'Comp B', - machineId: 'machine-1', - typeComposantId: 'type-root', - typeComposant: { - id: 'type-root', - name: 'Root type', - code: 'root', - componentSkeleton: { - customFields: [{ key: 'color', value: 'red' }], - pieces: [], - subcomponents: [], - }, - customFields: [], - }, - machine: null, - parentComposant: null, - typeMachineComponentRequirement: null, - constructeur: null, - customFieldValues: [], - pieces: [], - documents: [], - }; - - prisma.composant.create - .mockResolvedValueOnce(rootComponent) - .mockResolvedValueOnce({ - id: 'component-child', - name: 'Child component', - machineId: 'machine-1', - parentComposantId: 'component-1', - typeComposantId: 'type-child', - }); - - prisma.composant.findUnique.mockResolvedValue(rootComponent); - prisma.composant.findMany.mockResolvedValue([ - { ...rootComponent, parentComposantId: null }, - { - id: 'component-child', - name: 'Child component', - machineId: 'machine-1', - parentComposantId: 'component-1', - typeComposantId: 'type-child', - machine: null, - parentComposant: rootComponent, - typeComposant: null, - typeMachineComponentRequirement: null, - constructeur: null, - customFieldValues: [], - pieces: [], - documents: [], - }, - ]); - - await service.create(dto); - - expect(prisma.customField.findMany).toHaveBeenCalledWith({ - where: { typeComposantId: 'type-root' }, - select: { id: true, name: true }, - }); - expect(prisma.customFieldValue.create).toHaveBeenCalledWith({ - data: { - customFieldId: 'cf-color', - composantId: 'component-1', - value: 'red', - }, - }); - expect(prisma.piece.create).toHaveBeenCalledWith({ - data: { - name: 'Primary piece', - machineId: 'machine-1', - composantId: 'component-1', - typePieceId: 'type-piece', - typeMachinePieceRequirementId: 'req-piece', - }, - }); - expect(prisma.composant.create).toHaveBeenCalledTimes(2); - expect(prisma.composant.create.mock.calls[1][0].data).toMatchObject({ - parentComposantId: 'component-1', - typeComposantId: 'type-child', - typeMachineComponentRequirementId: 'req-child', - }); + expect(prisma.composant.update).toHaveBeenCalled(); }); }); diff --git a/src/composants/composants.service.ts b/src/composants/composants.service.ts index 6ac4059..5fb98f3 100644 --- a/src/composants/composants.service.ts +++ b/src/composants/composants.service.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { Prisma } from '@prisma/client'; import { PrismaService } from '../prisma/prisma.service'; import { @@ -9,638 +9,99 @@ import { COMPONENT_WITH_RELATIONS_INCLUDE, ComposantWithRelations, } from '../common/constants/component-includes'; -import { - buildComponentHierarchy, - buildComponentSubtree, -} from '../common/utils/component-tree.util'; -import { ComponentModelStructureSchema } from '../shared/schemas/inventory'; -import type { ComponentModelStructure } from '../shared/types/inventory'; - -type ComponentRequirementWithType = - Prisma.TypeMachineComponentRequirementGetPayload<{ - include: { typeComposant: true }; - }>; -type PieceRequirementWithType = Prisma.TypeMachinePieceRequirementGetPayload<{ - include: { typePiece: true }; -}>; -type ModelTypeWithSkeleton = ComponentRequirementWithType['typeComposant']; -type PieceTypeWithSkeleton = PieceRequirementWithType['typePiece']; @Injectable() export class ComposantsService { constructor(private prisma: PrismaService) {} - private async fetchComponentsByMachine( - machineId: string, - ): Promise { - return this.prisma.composant.findMany({ - where: { machineId }, - include: COMPONENT_WITH_RELATIONS_INCLUDE, - }) as Promise; - } + private buildCreateInput( + createComposantDto: CreateComposantDto, + ): Prisma.ComposantCreateInput { + const data: Prisma.ComposantCreateInput = { + name: createComposantDto.name, + reference: createComposantDto.reference ?? null, + prix: + createComposantDto.prix !== undefined ? createComposantDto.prix : null, + }; - private async getComponentWithHierarchy( - id: string, - ): Promise { - const baseComponent = (await this.prisma.composant.findUnique({ - where: { id }, - include: COMPONENT_WITH_RELATIONS_INCLUDE, - })) as ComposantWithRelations | null; - - if (!baseComponent) { - return null; + if (createComposantDto.constructeurId) { + data.constructeur = { + connect: { id: createComposantDto.constructeurId }, + }; } - if (!baseComponent.machineId) { - baseComponent.sousComposants = []; - return baseComponent; + if (createComposantDto.typeComposantId) { + data.typeComposant = { + connect: { id: createComposantDto.typeComposantId }, + }; } - const components = await this.fetchComponentsByMachine( - baseComponent.machineId, - ); - const subtree = buildComponentSubtree(components, id); - return subtree ?? baseComponent; + if (createComposantDto.structure !== undefined) { + data.structure = createComposantDto.structure as Prisma.InputJsonValue; + } + + return data; } async create(createComposantDto: CreateComposantDto) { - const requirementId = - createComposantDto.typeMachineComponentRequirementId ?? null; - - if (requirementId && !createComposantDto.machineId) { - throw new BadRequestException( - 'Un requirement ne peut pas être utilisé sans machine ciblée.', - ); - } - - let machineId = createComposantDto.machineId ?? null; - - if (createComposantDto.parentComposantId) { - const parentMachineId = await this.resolveMachineIdFromComposant( - createComposantDto.parentComposantId, - ); - - if (machineId && parentMachineId && machineId !== parentMachineId) { - throw new BadRequestException( - 'Le composant parent ne correspond pas à la machine ciblée.', - ); - } - - machineId = parentMachineId ?? machineId; - } - - let requirement: ComponentRequirementWithType | null = null; - let componentRequirements: ComponentRequirementWithType[] = []; - let pieceRequirements: PieceRequirementWithType[] = []; - - if (machineId) { - const machine = await this.prisma.machine.findUnique({ - where: { id: machineId }, - include: { - typeMachine: { - include: { - componentRequirements: { - include: { - typeComposant: true, - }, - }, - pieceRequirements: { - include: { - typePiece: true, - }, - }, - }, - }, - }, - }); - - if (!machine || !machine.typeMachine) { - throw new BadRequestException( - 'La machine ciblée doit être associée à un type de machine pour valider les requirements.', - ); - } - - componentRequirements = - (machine.typeMachine - .componentRequirements as ComponentRequirementWithType[]) ?? []; - pieceRequirements = - (machine.typeMachine.pieceRequirements as PieceRequirementWithType[]) ?? - []; - - if (requirementId) { - requirement = - componentRequirements.find( - (componentRequirement) => componentRequirement.id === requirementId, - ) ?? null; - - if (!requirement) { - throw new BadRequestException( - 'Le requirement de composant fourni ne correspond pas au squelette de la machine.', - ); - } - - if ( - createComposantDto.typeComposantId && - createComposantDto.typeComposantId !== requirement.typeComposantId - ) { - throw new BadRequestException( - 'Le type de composant fourni ne correspond pas au requirement pour cette machine.', - ); - } - } - } - - const typeComposantId = - createComposantDto.typeComposantId ?? - requirement?.typeComposantId ?? - null; - - const data: Prisma.ComposantUncheckedCreateInput = { - name: createComposantDto.name, - reference: createComposantDto.reference ?? null, - constructeurId: createComposantDto.constructeurId ?? null, - prix: - createComposantDto.prix !== undefined ? createComposantDto.prix : null, - machineId, - parentComposantId: createComposantDto.parentComposantId ?? null, - typeComposantId, - typeMachineComponentRequirementId: - requirement?.id ?? requirementId ?? null, - }; - - const created = (await this.prisma.composant.create({ - data, + const created = await this.prisma.composant.create({ + data: this.buildCreateInput(createComposantDto), include: COMPONENT_WITH_RELATIONS_INCLUDE, - })) as ComposantWithRelations; + }); - if (machineId && requirement?.id) { - const componentRequirementUsage = new Map(); - componentRequirementUsage.set(requirement.id, 1); - const pieceRequirementUsage = new Map(); - - await this.populateComponentFromSkeleton({ - componentId: created.id, - componentName: created.name, - componentType: - (requirement.typeComposant as ModelTypeWithSkeleton | null) ?? - (created.typeComposant as ModelTypeWithSkeleton | null) ?? - null, - machineId, - componentRequirements, - pieceRequirements, - componentRequirementUsage, - pieceRequirementUsage, - }); - } - - const component = await this.getComponentWithHierarchy(created.id); - return component ?? created; + return created as ComposantWithRelations; } async findAll() { - const components = (await this.prisma.composant.findMany({ + return (await this.prisma.composant.findMany({ include: COMPONENT_WITH_RELATIONS_INCLUDE, + orderBy: { name: 'asc' }, })) as ComposantWithRelations[]; - - return buildComponentHierarchy(components); } async findOne(id: string) { - return this.getComponentWithHierarchy(id); - } - - async findByMachine(machineId: string) { - const components = await this.fetchComponentsByMachine(machineId); - return buildComponentHierarchy(components); - } - - async findHierarchy(machineId: string) { - const components = await this.fetchComponentsByMachine(machineId); - return buildComponentHierarchy(components); + return (await this.prisma.composant.findUnique({ + where: { id }, + include: COMPONENT_WITH_RELATIONS_INCLUDE, + })) as ComposantWithRelations | null; } async update(id: string, updateComposantDto: UpdateComposantDto) { - const updated = (await this.prisma.composant.update({ + const data: Prisma.ComposantUpdateInput = {}; + + if (updateComposantDto.name !== undefined) { + data.name = updateComposantDto.name; + } + + if (updateComposantDto.reference !== undefined) { + data.reference = updateComposantDto.reference; + } + + if (updateComposantDto.prix !== undefined) { + data.prix = updateComposantDto.prix; + } + + if (updateComposantDto.constructeurId !== undefined) { + data.constructeur = updateComposantDto.constructeurId + ? { connect: { id: updateComposantDto.constructeurId } } + : { disconnect: true }; + } + + if (updateComposantDto.typeComposantId !== undefined) { + data.typeComposant = updateComposantDto.typeComposantId + ? { connect: { id: updateComposantDto.typeComposantId } } + : { disconnect: true }; + } + + if (updateComposantDto.structure !== undefined) { + data.structure = updateComposantDto.structure as Prisma.InputJsonValue; + } + + return (await this.prisma.composant.update({ where: { id }, - data: updateComposantDto, + data, include: COMPONENT_WITH_RELATIONS_INCLUDE, })) as ComposantWithRelations; - - return this.getComponentWithHierarchy(updated.id); - } - - private async populateComponentFromSkeleton({ - componentId, - componentName, - componentType, - machineId, - componentRequirements, - pieceRequirements, - componentRequirementUsage, - pieceRequirementUsage, - }: { - componentId: string; - componentName?: string; - componentType: ModelTypeWithSkeleton | null; - machineId: string; - componentRequirements: ComponentRequirementWithType[]; - pieceRequirements: PieceRequirementWithType[]; - componentRequirementUsage: Map; - pieceRequirementUsage: Map; - }) { - const skeleton = this.parseComponentSkeleton( - (componentType as { componentSkeleton?: Prisma.JsonValue | null } | null) - ?.componentSkeleton, - ); - if (!skeleton) { - return; - } - - await this.createComponentCustomFieldValues( - componentId, - componentType?.id ?? null, - skeleton.customFields, - ); - - await this.createPiecesFromSkeleton({ - componentId, - componentName, - machineId, - pieces: skeleton.pieces, - pieceRequirements, - pieceRequirementUsage, - }); - - for (const subcomponent of skeleton.subcomponents ?? []) { - const requirement = this.resolveComponentRequirement( - subcomponent, - componentRequirements, - componentRequirementUsage, - ); - - if (!requirement?.typeComposant) { - continue; - } - - const name = this.buildComponentName( - subcomponent, - requirement.typeComposant, - componentName, - ); - - const createdChild = await this.prisma.composant.create({ - data: { - name, - machineId, - parentComposantId: componentId, - typeComposantId: requirement.typeComposantId, - typeMachineComponentRequirementId: requirement.id, - }, - }); - - this.incrementRequirementUsage(componentRequirementUsage, requirement.id); - - await this.populateComponentFromSkeleton({ - componentId: createdChild.id, - componentName: createdChild.name, - componentType: requirement.typeComposant, - machineId, - componentRequirements, - pieceRequirements, - componentRequirementUsage, - pieceRequirementUsage, - }); - } - } - - private parseComponentSkeleton( - value: unknown, - ): ComponentModelStructure | null { - if (!value) { - return null; - } - - try { - return ComponentModelStructureSchema.parse(value); - } catch (error) { - return null; - } - } - - private async createComponentCustomFieldValues( - componentId: string, - typeComposantId: string | null, - customFields: ComponentModelStructure['customFields'], - ) { - if ( - !typeComposantId || - !Array.isArray(customFields) || - customFields.length === 0 - ) { - return; - } - - const definitions = await this.prisma.customField.findMany({ - where: { typeComposantId }, - select: { id: true, name: true }, - }); - - if (definitions.length === 0) { - return; - } - - const definitionMap = new Map( - definitions.map((field) => [field.name, field.id]), - ); - const existingValues = await this.prisma.customFieldValue.findMany({ - where: { composantId: componentId }, - select: { customFieldId: true }, - }); - const existingIds = new Set( - existingValues.map((value) => value.customFieldId), - ); - - for (const field of customFields) { - const key = this.normalizeIdentifier(field?.key); - if (!key) { - continue; - } - - const definitionId = definitionMap.get(key); - if (!definitionId || existingIds.has(definitionId)) { - continue; - } - - await this.prisma.customFieldValue.create({ - data: { - customFieldId: definitionId, - composantId: componentId, - value: this.toCustomFieldValue(field?.value), - }, - }); - - existingIds.add(definitionId); - } - } - - private async createPiecesFromSkeleton({ - componentId, - componentName, - machineId, - pieces, - pieceRequirements, - pieceRequirementUsage, - }: { - componentId: string; - componentName?: string; - machineId: string; - pieces: ComponentModelStructure['pieces']; - pieceRequirements: PieceRequirementWithType[]; - pieceRequirementUsage: Map; - }) { - if (!Array.isArray(pieces) || pieces.length === 0) { - return; - } - - for (const entry of pieces) { - const requirement = this.resolvePieceRequirement( - entry, - pieceRequirements, - pieceRequirementUsage, - ); - - if (!requirement?.typePiece) { - continue; - } - - const name = this.buildPieceName( - entry, - requirement.typePiece, - componentName, - ); - - await this.prisma.piece.create({ - data: { - name, - machineId, - composantId: componentId, - typePieceId: requirement.typePieceId, - typeMachinePieceRequirementId: requirement.id, - }, - }); - - this.incrementRequirementUsage(pieceRequirementUsage, requirement.id); - } - } - - private resolveComponentRequirement( - entry: ComponentModelStructure['subcomponents'][number], - requirements: ComponentRequirementWithType[], - usage: Map, - ): ComponentRequirementWithType | null { - const typeComposantId = this.normalizeIdentifier( - (entry as { typeComposantId?: string }).typeComposantId, - ); - const familyCode = this.normalizeCode( - (entry as { familyCode?: string }).familyCode, - ); - - const candidates = requirements.filter((requirement) => { - if (typeComposantId && requirement.typeComposantId === typeComposantId) { - return true; - } - - if (familyCode && requirement.typeComposant?.code) { - return ( - this.normalizeCode(requirement.typeComposant.code) === familyCode - ); - } - - return false; - }); - - if (candidates.length === 0) { - if (typeComposantId || familyCode) { - throw new BadRequestException( - `Aucun requirement de composant ne correspond au squelette (${typeComposantId ?? familyCode}).`, - ); - } - - throw new BadRequestException( - 'Le squelette du composant référence un sous-composant sans identifiant de type.', - ); - } - - for (const candidate of candidates) { - if (this.hasRequirementCapacity(candidate, usage)) { - return candidate; - } - } - - throw new BadRequestException( - `La capacité maximale du requirement de composant (${typeComposantId ?? familyCode}) est atteinte pour la machine visée.`, - ); - } - - private resolvePieceRequirement( - entry: ComponentModelStructure['pieces'][number], - requirements: PieceRequirementWithType[], - usage: Map, - ): PieceRequirementWithType | null { - const typePieceId = this.normalizeIdentifier( - (entry as { typePieceId?: string }).typePieceId, - ); - const familyCode = this.normalizeCode( - (entry as { familyCode?: string }).familyCode, - ); - - const candidates = requirements.filter((requirement) => { - if (typePieceId && requirement.typePieceId === typePieceId) { - return true; - } - - if (familyCode && requirement.typePiece?.code) { - return this.normalizeCode(requirement.typePiece.code) === familyCode; - } - - return false; - }); - - if (candidates.length === 0) { - if (typePieceId || familyCode) { - throw new BadRequestException( - `Aucun requirement de pièce ne correspond au squelette (${typePieceId ?? familyCode}).`, - ); - } - - throw new BadRequestException( - 'Le squelette du composant référence une pièce sans identifiant de type.', - ); - } - - for (const candidate of candidates) { - if (this.hasRequirementCapacity(candidate, usage)) { - return candidate; - } - } - - throw new BadRequestException( - `La capacité maximale du requirement de pièce (${typePieceId ?? familyCode}) est atteinte pour la machine visée.`, - ); - } - - private hasRequirementCapacity( - requirement: { id: string; maxCount: number | null | undefined }, - usage: Map, - ): boolean { - const max = requirement.maxCount; - if (max === null || max === undefined) { - return true; - } - - const current = usage.get(requirement.id) ?? 0; - return current < max; - } - - private incrementRequirementUsage(usage: Map, id: string) { - usage.set(id, (usage.get(id) ?? 0) + 1); - } - - private buildComponentName( - subcomponent: ComponentModelStructure['subcomponents'][number], - typeComposant: ModelTypeWithSkeleton | null, - parentName?: string, - ): string { - const alias = this.normalizeIdentifier( - (subcomponent as { alias?: string }).alias, - ); - if (alias) { - return alias; - } - - if (typeComposant?.name) { - return typeComposant.name; - } - - if (parentName) { - return `${parentName} - Sous-composant`; - } - - return 'Sous-composant'; - } - - private buildPieceName( - piece: ComponentModelStructure['pieces'][number], - typePiece: PieceTypeWithSkeleton | null, - componentName?: string, - ): string { - const role = this.normalizeIdentifier((piece as { role?: string }).role); - if (role) { - return role; - } - - if (typePiece?.name) { - return typePiece.name; - } - - if (componentName) { - return `${componentName} - Pièce`; - } - - return 'Pièce'; - } - - private normalizeIdentifier(value: unknown): string | null { - if (typeof value !== 'string') { - return null; - } - - const trimmed = value.trim(); - return trimmed.length > 0 ? trimmed : null; - } - - private normalizeCode(value: unknown): string | null { - const identifier = this.normalizeIdentifier(value); - return identifier ? identifier.toLowerCase() : null; - } - - private toCustomFieldValue(value: unknown): string { - if (value === undefined || value === null) { - return ''; - } - - return String(value); - } - - private async resolveMachineIdFromComposant( - composantId: string, - ): Promise { - const composant = await this.prisma.composant.findUnique({ - where: { id: composantId }, - select: { - id: true, - machineId: true, - parentComposantId: true, - }, - }); - - if (!composant) { - throw new BadRequestException( - 'Le composant parent spécifié est introuvable.', - ); - } - - if (composant.machineId) { - return composant.machineId; - } - - if (composant.parentComposantId) { - return this.resolveMachineIdFromComposant(composant.parentComposantId); - } - - throw new BadRequestException( - 'Impossible de déterminer la machine associée au composant parent.', - ); } async remove(id: string) { diff --git a/src/machines/machines.service.ts b/src/machines/machines.service.ts index 9e70604..7cd674c 100644 --- a/src/machines/machines.service.ts +++ b/src/machines/machines.service.ts @@ -1,16 +1,15 @@ +import { randomUUID } from 'crypto'; import { Injectable } from '@nestjs/common'; -import { Prisma, ModelCategory } from '@prisma/client'; +import { Prisma } from '@prisma/client'; import { PrismaService } from '../prisma/prisma.service'; import { CreateMachineDto, UpdateMachineDto, ReconfigureMachineDto, - MachineComponentSelectionDto, - MachinePieceSelectionDto, + MachineComponentLinkInput, + MachinePieceLinkInput, } from '../shared/dto/machine.dto'; import { buildComponentHierarchy } from '../common/utils/component-tree.util'; -import { ComposantsService } from '../composants/composants.service'; -import { PiecesService } from '../pieces/pieces.service'; const CUSTOM_FIELD_SELECT = { id: true, @@ -42,7 +41,7 @@ const TYPE_MACHINE_CONFIGURATION_INCLUDE: Prisma.TypeMachineInclude = { }, }; -const MACHINE_PIECE_LINK_INCLUDE = { +const MACHINE_PIECE_LINK_INCLUDE: Prisma.MachinePieceLinkInclude = { piece: { include: { customFieldValues: { @@ -70,36 +69,50 @@ const MACHINE_PIECE_LINK_INCLUDE = { }, } satisfies Prisma.MachinePieceLinkInclude; -const MACHINE_COMPONENT_LINK_INCLUDE = { - composant: { - include: { - constructeur: true, - typeComposant: { - include: { - customFields: true, +const buildComponentLinkInclude = ( + depth = 5, +): Prisma.MachineComponentLinkInclude => { + const include: Prisma.MachineComponentLinkInclude = { + composant: { + include: { + constructeur: true, + typeComposant: { + include: { + customFields: true, + }, }, - }, - customFieldValues: { - include: { - customField: { select: CUSTOM_FIELD_SELECT }, + customFieldValues: { + include: { + customField: { select: CUSTOM_FIELD_SELECT }, + }, }, + documents: true, }, - documents: true, }, - }, - typeMachineComponentRequirement: { - include: { - typeComposant: { - include: { - customFields: true, + typeMachineComponentRequirement: { + include: { + typeComposant: { + include: { + customFields: true, + }, }, }, }, - }, - pieceLinks: { - include: MACHINE_PIECE_LINK_INCLUDE, - }, -} satisfies Prisma.MachineComponentLinkInclude; + pieceLinks: { + include: MACHINE_PIECE_LINK_INCLUDE, + }, + }; + + if (depth > 1) { + include.childLinks = { + include: buildComponentLinkInclude(depth - 1), + }; + } + + return include; +}; + +const MACHINE_COMPONENT_LINK_INCLUDE = buildComponentLinkInclude(); const MACHINE_DEFAULT_INCLUDE = { site: true, @@ -182,12 +195,59 @@ type PieceRequirementWithType = Prisma.TypeMachinePieceRequirementGetPayload<{ include: { typePiece: true }; }>; +type ComponentWithType = Prisma.ComposantGetPayload<{ + include: { typeComposant: true }; +}>; + +type PieceWithType = Prisma.PieceGetPayload<{ + include: { typePiece: true }; +}>; + +type CreatedComponentLinkInfo = { + id: string; + composantId: string; + requirementId: string | null; +}; + +type PendingComponentLink = { + raw: MachineComponentLinkInput; + assignedId: string; + requirement: ComponentRequirementWithType; + componentId: string; + component?: ComponentWithType; + overrideMutation?: { + nameOverride?: string | null; + referenceOverride?: string | null; + prixOverride?: Prisma.Decimal | null; + }; + position: number; +}; + +type CreatedPieceLinkInfo = { + id: string; + pieceId: string; + requirementId: string; + parentLinkId: string | null; +}; + +type PendingPieceLink = { + raw: MachinePieceLinkInput; + assignedId: string; + requirement: PieceRequirementWithType; + pieceId: string; + piece?: PieceWithType; + overrideMutation?: { + nameOverride?: string | null; + referenceOverride?: string | null; + prixOverride?: Prisma.Decimal | null; + }; + position: number; +}; + @Injectable() export class MachinesService { constructor( private prisma: PrismaService, - private composantsService: ComposantsService, - private piecesService: PiecesService, ) {} private toLinkOverride(source: { @@ -280,7 +340,9 @@ export class MachinesService { }; const hydratedPieces = Array.isArray(pieceLinks) - ? pieceLinks.map((pieceLink) => this.hydratePieceLink(pieceLink, summary)) + ? (pieceLinks as MachinePieceLinkWithRelations[]).map((pieceLink) => + this.hydratePieceLink(pieceLink, summary), + ) : []; const hydratedLink: HydratedComponentLink = { @@ -401,8 +463,8 @@ export class MachinesService { private buildConfigurationContext( typeMachine: TypeMachineConfiguration, - componentSelections: MachineComponentSelectionDto[], - pieceSelections: MachinePieceSelectionDto[], + componentLinks: MachineComponentLinkInput[], + pieceLinks: MachinePieceLinkInput[], ) { const componentRequirements = ( Array.isArray(typeMachine.componentRequirements) @@ -422,63 +484,60 @@ export class MachinesService { pieceRequirements.map((requirement) => [requirement.id, requirement]), ); - const componentSelectionMap = new Map< + const componentLinksByRequirement = new Map< string, - MachineComponentSelectionDto[] + MachineComponentLinkInput[] >(); - for (const selection of componentSelections) { - const requirement = componentRequirementMap.get(selection.requirementId); + for (const link of componentLinks) { + const requirement = componentRequirementMap.get(link.requirementId); if (!requirement) { throw new Error( - `Sélection de composant invalide: requirementId=${selection.requirementId}`, + `Lien de composant invalide: requirementId=${link.requirementId}`, ); } if ( - selection.typeComposantId && - selection.typeComposantId !== requirement.typeComposantId + link.typeComposantId && + link.typeComposantId !== requirement.typeComposantId ) { throw new Error( - `Le type de composant sélectionné ne correspond pas au requirement ${requirement.id}.`, + `Le composant sélectionné ne correspond pas au requirement ${requirement.id}.`, ); } - if (!componentSelectionMap.has(requirement.id)) { - componentSelectionMap.set(requirement.id, []); + if (!componentLinksByRequirement.has(requirement.id)) { + componentLinksByRequirement.set(requirement.id, []); } - componentSelectionMap.get(requirement.id)!.push(selection); + componentLinksByRequirement.get(requirement.id)!.push(link); } - const pieceSelectionMap = new Map(); - for (const selection of pieceSelections) { - const requirement = pieceRequirementMap.get(selection.requirementId); + const pieceLinksByRequirement = new Map(); + for (const link of pieceLinks) { + const requirement = pieceRequirementMap.get(link.requirementId); if (!requirement) { throw new Error( - `Sélection de pièce invalide: requirementId=${selection.requirementId}`, + `Lien de pièce invalide: requirementId=${link.requirementId}`, ); } - if ( - selection.typePieceId && - selection.typePieceId !== requirement.typePieceId - ) { + if (link.typePieceId && link.typePieceId !== requirement.typePieceId) { throw new Error( - `Le type de pièce sélectionné ne correspond pas au requirement ${requirement.id}.`, + `La pièce sélectionnée ne correspond pas au requirement ${requirement.id}.`, ); } - if (!pieceSelectionMap.has(requirement.id)) { - pieceSelectionMap.set(requirement.id, []); + if (!pieceLinksByRequirement.has(requirement.id)) { + pieceLinksByRequirement.set(requirement.id, []); } - pieceSelectionMap.get(requirement.id)!.push(selection); + pieceLinksByRequirement.get(requirement.id)!.push(link); } for (const requirement of componentRequirements) { - const selections = componentSelectionMap.get(requirement.id) ?? []; + const linksForRequirement = componentLinksByRequirement.get(requirement.id) ?? []; const min = requirement.minCount ?? (requirement.required ? 1 : 0); const max = requirement.maxCount ?? undefined; - if (selections.length < min) { + if (linksForRequirement.length < min) { throw new Error( `Le groupe de composants "${ requirement.label || @@ -488,7 +547,7 @@ export class MachinesService { ); } - if (max !== undefined && selections.length > max) { + if (max !== undefined && linksForRequirement.length > max) { throw new Error( `Le groupe de composants "${ requirement.label || @@ -500,11 +559,11 @@ export class MachinesService { } for (const requirement of pieceRequirements) { - const selections = pieceSelectionMap.get(requirement.id) ?? []; + const linksForRequirement = pieceLinksByRequirement.get(requirement.id) ?? []; const min = requirement.minCount ?? (requirement.required ? 1 : 0); const max = requirement.maxCount ?? undefined; - if (selections.length < min) { + if (linksForRequirement.length < min) { throw new Error( `Le groupe de pièces "${ requirement.label || requirement.typePiece?.name || requirement.id @@ -512,7 +571,7 @@ export class MachinesService { ); } - if (max !== undefined && selections.length > max) { + if (max !== undefined && linksForRequirement.length > max) { throw new Error( `Le groupe de pièces "${ requirement.label || requirement.typePiece?.name || requirement.id @@ -522,8 +581,10 @@ export class MachinesService { } return { - componentSelectionMap, - pieceSelectionMap, + componentRequirementMap, + pieceRequirementMap, + componentLinksByRequirement, + pieceLinksByRequirement, }; } @@ -631,146 +692,915 @@ export class MachinesService { return undefined; } - private async attachExistingComponentToMachine( - machineId: string, - requirement: ComponentRequirementWithType, - selection: MachineComponentSelectionDto, - ) { - const componentId = selection.composantId; - if (!componentId) { - throw new Error('composantId manquant pour la sélection.'); + private resolveLinkIdentifier(link: { id?: string; linkId?: string }): string | undefined { + const candidate = + (typeof link.id === 'string' && link.id.trim()) + ? link.id.trim() + : typeof link.linkId === 'string' + ? link.linkId.trim() + : undefined; + + return candidate && candidate.length > 0 ? candidate : undefined; + } + + private buildLinkOverrideMutation( + overrides?: Record, + ): { + nameOverride?: string | null; + referenceOverride?: string | null; + prixOverride?: Prisma.Decimal | null; + } { + if (!overrides) { + return {}; } - const component = await this.prisma.composant.findUnique({ - where: { id: componentId }, + const container = this.ensurePlainObject(overrides); + const mutation: { + nameOverride?: string | null; + referenceOverride?: string | null; + prixOverride?: Prisma.Decimal | null; + } = {}; + + if ( + Object.prototype.hasOwnProperty.call(container, 'name') || + Object.prototype.hasOwnProperty.call(container, 'nom') + ) { + const value = this.extractString( + container.name ?? container.nom, + ); + mutation.nameOverride = value ?? null; + } + + if ( + Object.prototype.hasOwnProperty.call(container, 'reference') || + Object.prototype.hasOwnProperty.call(container, 'ref') || + Object.prototype.hasOwnProperty.call(container, 'code') + ) { + const value = this.extractString( + container.reference ?? container.ref ?? container.code, + ); + mutation.referenceOverride = value ?? null; + } + + if ( + Object.prototype.hasOwnProperty.call(container, 'prix') || + Object.prototype.hasOwnProperty.call(container, 'price') || + Object.prototype.hasOwnProperty.call(container, 'prixOverride') || + Object.prototype.hasOwnProperty.call(container, 'priceOverride') + ) { + const rawPrice = + container.prix ?? + container.price ?? + container.prixOverride ?? + container.priceOverride; + const normalized = this.normalizePrice(rawPrice); + if (normalized === undefined) { + throw new Error('La valeur de prix fournie dans les overrides est invalide.'); + } + mutation.prixOverride = + normalized === null ? null : new Prisma.Decimal(normalized); + } + + return mutation; + } + + private describeRequirement( + requirement: ComponentRequirementWithType | PieceRequirementWithType, + ): string { + return ( + requirement.label || + (requirement as ComponentRequirementWithType).typeComposant?.name || + (requirement as PieceRequirementWithType).typePiece?.name || + requirement.id + ); + } + + private describeComponent(component: ComponentWithType): string { + return component.name || component.reference || component.id; + } + + private describePiece(piece: PieceWithType): string { + return piece.name || piece.reference || piece.id; + } + + private extractStructureSubcomponents( + structure: Record | null | undefined, + ) { + if (!structure || typeof structure !== 'object') { + return []; + } + + const direct = Array.isArray((structure as any).subcomponents) + ? (structure as any).subcomponents + : []; + const legacy = Array.isArray((structure as any).subComponents) + ? (structure as any).subComponents + : []; + + return [...direct, ...legacy]; + } + + private extractStructurePieces(structure: Record | null | undefined) { + if (!structure || typeof structure !== 'object') { + return []; + } + + const pieces = Array.isArray((structure as any).pieces) + ? (structure as any).pieces + : []; + const legacy = Array.isArray((structure as any).pieceLinks) + ? (structure as any).pieceLinks + : []; + + return [...pieces, ...legacy]; + } + + private extractStructureAlias(entry: Record | null | undefined) { + if (!entry || typeof entry !== 'object') { + return null; + } + + const aliasCandidate = + this.extractString((entry as any).alias) ?? + this.extractString((entry as any).name); + + if (aliasCandidate) { + return aliasCandidate; + } + + const definition = this.ensurePlainObject((entry as any).definition); + return this.extractString(definition.alias ?? definition.name) ?? null; + } + + private extractStructureReference( + entry: Record | null | undefined, + ) { + if (!entry || typeof entry !== 'object') { + return null; + } + + const direct = this.extractString((entry as any).reference); + if (direct) { + return direct; + } + + const definition = this.ensurePlainObject((entry as any).definition); + return this.extractString(definition.reference) ?? null; + } + + private extractStructurePieceName(entry: Record | null | undefined) { + if (!entry || typeof entry !== 'object') { + return null; + } + + const direct = + this.extractString((entry as any).name) ?? + this.extractString((entry as any).alias) ?? + this.extractString((entry as any).label) ?? + this.extractString((entry as any).role); + + if (direct) { + return direct; + } + + const definition = this.ensurePlainObject((entry as any).definition); + return ( + this.extractString(definition.alias) ?? + this.extractString(definition.name) ?? + this.extractString(definition.label) ?? + this.extractString(definition.role) ?? + null + ); + } + + private extractStructurePieceReference( + entry: Record | null | undefined, + ) { + if (!entry || typeof entry !== 'object') { + return null; + } + + const direct = + this.extractString((entry as any).reference) ?? + this.extractString((entry as any).ref) ?? + this.extractString((entry as any).code); + + if (direct) { + return direct; + } + + const definition = this.ensurePlainObject((entry as any).definition); + return ( + this.extractString(definition.reference) ?? + this.extractString(definition.ref) ?? + this.extractString(definition.code) ?? + null + ); + } + + private extractStructurePiecePrice( + entry: Record | null | undefined, + ): number | null | undefined { + if (!entry || typeof entry !== 'object') { + return undefined; + } + + const definition = this.ensurePlainObject((entry as any).definition); + const raw = + (entry as any).prix ?? + (entry as any).price ?? + definition.prix ?? + definition.price ?? + null; + + return this.normalizePrice(raw); + } + + private async autoCreateStructureComponentLinks( + prisma: Prisma.TransactionClient | PrismaService, + machineId: string, + createdLinks: Map, + byComponentId: Map, + componentMap: Map, + ) { + if (createdLinks.size === 0) { + return; + } + + const componentCache = new Map(componentMap); + const pieceCache = new Map(); + + const ensureComponent = async (componentId: string) => { + if (componentCache.has(componentId)) { + return componentCache.get(componentId)!; + } + + const fetched = await prisma.composant.findUnique({ + where: { id: componentId }, + include: { + typeComposant: true, + }, + }); + + if (fetched) { + componentCache.set(componentId, fetched as ComponentWithType); + return fetched as ComponentWithType; + } + + return undefined; + }; + + const ensurePiece = async (pieceId: string) => { + if (pieceCache.has(pieceId)) { + return pieceCache.get(pieceId)!; + } + + const fetched = await prisma.piece.findUnique({ + where: { id: pieceId }, + include: { + typePiece: true, + constructeur: true, + }, + }); + + if (fetched) { + pieceCache.set(pieceId, fetched as PieceWithType); + return fetched as PieceWithType; + } + + return undefined; + }; + + const createdChildKeys = new Set(); + const createdPieceKeys = new Set(); + const queue: CreatedComponentLinkInfo[] = Array.from(createdLinks.values()); + + while (queue.length > 0) { + const linkInfo = queue.shift(); + if (!linkInfo) { + continue; + } + + const component = await ensureComponent(linkInfo.composantId); + if (!component) { + continue; + } + + const structure = this.ensurePlainObject(component.structure); + const subcomponents = this.extractStructureSubcomponents(structure); + + const structurePieces = this.extractStructurePieces(structure); + if (structurePieces.length) { + for (const rawPiece of structurePieces) { + const pieceEntry = this.ensurePlainObject(rawPiece); + const selectedPieceId = this.extractString( + pieceEntry.selectedPieceId ?? + pieceEntry.pieceId ?? + pieceEntry.id, + ); + + if (!selectedPieceId) { + continue; + } + + const pieceKey = `${linkInfo.id}::piece::${selectedPieceId}`; + if (createdPieceKeys.has(pieceKey)) { + continue; + } + + const piece = await ensurePiece(selectedPieceId); + if (!piece) { + continue; + } + + const existingPieceLink = await prisma.machinePieceLink.findFirst({ + where: { + machineId, + pieceId: selectedPieceId, + }, + select: { + id: true, + parentLinkId: true, + nameOverride: true, + referenceOverride: true, + prixOverride: true, + }, + }); + + const pieceNameOverride = this.extractStructurePieceName(pieceEntry); + const pieceReferenceOverride = + this.extractStructurePieceReference(pieceEntry); + const rawPrice = this.extractStructurePiecePrice(pieceEntry); + const prixOverrideValue = + rawPrice === undefined + ? undefined + : rawPrice === null + ? null + : new Prisma.Decimal(rawPrice); + + if (existingPieceLink) { + if (existingPieceLink.parentLinkId !== linkInfo.id) { + await prisma.machinePieceLink.update({ + where: { id: existingPieceLink.id }, + data: { + parentLinkId: linkInfo.id, + nameOverride: + pieceNameOverride !== null && pieceNameOverride !== undefined + ? pieceNameOverride + : existingPieceLink.nameOverride, + referenceOverride: + pieceReferenceOverride !== null && + pieceReferenceOverride !== undefined + ? pieceReferenceOverride + : existingPieceLink.referenceOverride, + prixOverride: + prixOverrideValue !== undefined + ? prixOverrideValue + : existingPieceLink.prixOverride, + }, + }); + } else if ( + (pieceNameOverride !== null && pieceNameOverride !== undefined) || + (pieceReferenceOverride !== null && + pieceReferenceOverride !== undefined) || + prixOverrideValue !== undefined + ) { + await prisma.machinePieceLink.update({ + where: { id: existingPieceLink.id }, + data: { + nameOverride: + pieceNameOverride !== null && pieceNameOverride !== undefined + ? pieceNameOverride + : existingPieceLink.nameOverride, + referenceOverride: + pieceReferenceOverride !== null && + pieceReferenceOverride !== undefined + ? pieceReferenceOverride + : existingPieceLink.referenceOverride, + prixOverride: + prixOverrideValue !== undefined + ? prixOverrideValue + : existingPieceLink.prixOverride, + }, + }); + } + + createdPieceKeys.add(pieceKey); + continue; + } + + const assignedPieceId = randomUUID(); + + await prisma.machinePieceLink.create({ + data: { + id: assignedPieceId, + machineId, + pieceId: selectedPieceId, + parentLinkId: linkInfo.id, + typeMachinePieceRequirementId: null, + nameOverride: pieceNameOverride ?? null, + referenceOverride: pieceReferenceOverride ?? null, + prixOverride: + prixOverrideValue === undefined ? null : prixOverrideValue, + }, + }); + + createdPieceKeys.add(pieceKey); + } + } + + if (!subcomponents.length) { + continue; + } + + for (const rawEntry of subcomponents) { + const entry = this.ensurePlainObject(rawEntry); + const selectedComponentId = this.extractString( + entry.selectedComponentId ?? + entry.componentId ?? + entry.composantId, + ); + + if (!selectedComponentId) { + continue; + } + + const cacheKey = `${linkInfo.id}::${selectedComponentId}`; + if (createdChildKeys.has(cacheKey)) { + continue; + } + + const childComponent = await ensureComponent(selectedComponentId); + if (!childComponent) { + continue; + } + + const existingLink = await prisma.machineComponentLink.findFirst({ + where: { + machineId, + parentLinkId: linkInfo.id, + composantId: selectedComponentId, + }, + select: { id: true }, + }); + + if (existingLink) { + createdChildKeys.add(cacheKey); + continue; + } + + const assignedId = randomUUID(); + const alias = this.extractStructureAlias(entry); + const reference = this.extractStructureReference(entry); + + await prisma.machineComponentLink.create({ + data: { + id: assignedId, + machineId, + composantId: selectedComponentId, + parentLinkId: linkInfo.id, + typeMachineComponentRequirementId: null, + nameOverride: alias ?? null, + referenceOverride: reference ?? null, + }, + }); + + const created: CreatedComponentLinkInfo = { + id: assignedId, + composantId: selectedComponentId, + requirementId: null, + }; + + createdLinks.set(assignedId, created); + createdChildKeys.add(cacheKey); + + if (!byComponentId.has(selectedComponentId)) { + byComponentId.set(selectedComponentId, []); + } + byComponentId.get(selectedComponentId)!.push(created); + + queue.push(created); + } + } + } + + private async createComponentLinksForMachine( + prisma: Prisma.TransactionClient | PrismaService, + machineId: string, + componentRequirementMap: Map, + componentLinks: MachineComponentLinkInput[], + ) { + const links = Array.isArray(componentLinks) ? componentLinks : []; + if (links.length === 0) { + return { + createdLinks: new Map(), + byComponentId: new Map(), + byRequirementId: new Map(), + }; + } + + const componentIds = new Set(); + const pendingEntries: PendingComponentLink[] = []; + + links.forEach((link, index) => { + const requirement = componentRequirementMap.get(link.requirementId); + if (!requirement) { + throw new Error( + `Requirement de composant introuvable (${link.requirementId}).`, + ); + } + + const componentId = this.extractString( + (link.composantId as unknown) ?? (link.componentId as unknown), + ); + + if (!componentId) { + throw new Error( + `composantId manquant pour le lien de composant #${index + 1} (${this.describeRequirement(requirement)}).`, + ); + } + + componentIds.add(componentId); + + pendingEntries.push({ + raw: link, + assignedId: this.resolveLinkIdentifier(link) ?? randomUUID(), + requirement, + componentId, + position: index, + }); + }); + + const components = await prisma.composant.findMany({ + where: { id: { in: Array.from(componentIds) } }, include: { typeComposant: true }, }); + const componentMap = new Map( + components.map((component) => [component.id, component]), + ); - if (!component) { - throw new Error(`Composant introuvable (${componentId}).`); - } + for (const entry of pendingEntries) { + const component = componentMap.get(entry.componentId); + if (!component) { + throw new Error( + `Composant introuvable (${entry.componentId}) pour le lien de composant #${entry.position + 1}.`, + ); + } - if ( - requirement.typeComposantId && - component.typeComposantId && - component.typeComposantId !== requirement.typeComposantId - ) { - throw new Error( - `Le composant sélectionné (${component.name || component.id}) n'appartient pas à la famille attendue pour ce requirement.`, + if ( + entry.requirement.typeComposantId && + component.typeComposantId && + component.typeComposantId !== entry.requirement.typeComposantId + ) { + throw new Error( + `Le composant "${this.describeComponent(component)}" n'appartient pas à la famille attendue pour "${this.describeRequirement(entry.requirement)}".`, + ); + } + + entry.component = component; + entry.overrideMutation = this.buildLinkOverrideMutation( + this.ensurePlainObject(entry.raw.overrides), ); } - await this.prisma.composant.update({ - where: { id: component.id }, - data: { - machineId, - parentComposantId: null, - typeMachineComponentRequirementId: requirement.id, - }, - }); - } + const pending = new Map( + pendingEntries.map((entry) => [entry.assignedId, entry]), + ); - private async attachExistingPieceToMachine( - machineId: string, - requirement: PieceRequirementWithType, - selection: MachinePieceSelectionDto, - ) { - const pieceId = selection.pieceId; - if (!pieceId) { - throw new Error('pieceId manquant pour la sélection.'); + const createdLinks = new Map(); + const byComponentId = new Map(); + const byRequirementId = new Map(); + + while (pending.size > 0) { + let progress = false; + + for (const [id, entry] of pending) { + const parentResolution = this.resolveComponentParentReference( + entry.raw, + pending, + createdLinks, + byComponentId, + byRequirementId, + ); + + if (!parentResolution.ready) { + continue; + } + + const createData: Prisma.MachineComponentLinkUncheckedCreateInput = { + id: entry.assignedId, + machineId, + composantId: entry.componentId, + typeMachineComponentRequirementId: entry.requirement.id, + parentLinkId: parentResolution.parentId ?? null, + }; + + if (entry.overrideMutation?.nameOverride !== undefined) { + createData.nameOverride = entry.overrideMutation.nameOverride; + } + if (entry.overrideMutation?.referenceOverride !== undefined) { + createData.referenceOverride = entry.overrideMutation.referenceOverride; + } + if (entry.overrideMutation?.prixOverride !== undefined) { + createData.prixOverride = entry.overrideMutation.prixOverride; + } + + await prisma.machineComponentLink.create({ data: createData }); + + const created: CreatedComponentLinkInfo = { + id: entry.assignedId, + composantId: entry.componentId, + requirementId: entry.requirement.id, + }; + + createdLinks.set(entry.assignedId, created); + + if (!byComponentId.has(entry.componentId)) { + byComponentId.set(entry.componentId, []); + } + byComponentId.get(entry.componentId)!.push(created); + + if (!byRequirementId.has(entry.requirement.id)) { + byRequirementId.set(entry.requirement.id, []); + } + byRequirementId.get(entry.requirement.id)!.push(created); + + pending.delete(id); + progress = true; + } + + if (!progress) { + throw new Error( + 'Impossible de créer les liens de composants: une dépendance parent est manquante ou crée une boucle.', + ); + } } - const piece = await this.prisma.piece.findUnique({ - where: { id: pieceId }, + await this.autoCreateStructureComponentLinks( + prisma, + machineId, + createdLinks, + byComponentId, + componentMap, + ); + + return { createdLinks, byComponentId, byRequirementId }; + } + + private resolveComponentParentReference( + link: MachineComponentLinkInput, + pending: Map, + createdLinks: Map, + byComponentId: Map, + byRequirementId: Map, + ): { parentId: string | null; ready: boolean } { + const explicitParentId = this.extractString( + (link.parentLinkId as unknown) ?? link.parentComponentLinkId, + ); + + if (explicitParentId) { + if (createdLinks.has(explicitParentId)) { + return { parentId: explicitParentId, ready: true }; + } + if (pending.has(explicitParentId)) { + return { parentId: null, ready: false }; + } + throw new Error( + `Lien parent introuvable (${explicitParentId}) pour un composant sélectionné.`, + ); + } + + const parentReqId = this.extractString( + link.parentComponentRequirementId ?? + link.parentRequirementId ?? + link.parentMachineComponentRequirementId, + ); + + if (parentReqId) { + const matches = byRequirementId.get(parentReqId) ?? []; + if (matches.length === 1) { + return { parentId: matches[0].id, ready: true }; + } + if (matches.length > 1) { + throw new Error( + `Plusieurs liens de composant correspondent au requirement parent (${parentReqId}).`, + ); + } + const pendingMatch = Array.from(pending.values()).find( + (entry) => entry.requirement.id === parentReqId, + ); + if (pendingMatch) { + return { parentId: null, ready: false }; + } + throw new Error( + `Lien parent attendu pour le requirement ${parentReqId} introuvable.`, + ); + } + + const parentComponentId = this.extractString(link.parentComponentId); + if (parentComponentId) { + const matches = byComponentId.get(parentComponentId) ?? []; + if (matches.length === 1) { + return { parentId: matches[0].id, ready: true }; + } + if (matches.length > 1) { + throw new Error( + `Impossible de déterminer le lien parent pour le composant ${parentComponentId}: plusieurs occurrences trouvées.`, + ); + } + const pendingMatch = Array.from(pending.values()).find( + (entry) => entry.componentId === parentComponentId, + ); + if (pendingMatch) { + return { parentId: null, ready: false }; + } + throw new Error( + `Lien parent attendu pour le composant ${parentComponentId} introuvable.`, + ); + } + + return { parentId: null, ready: true }; + } + + private async createPieceLinksForMachine( + prisma: Prisma.TransactionClient | PrismaService, + machineId: string, + pieceRequirementMap: Map, + pieceLinks: MachinePieceLinkInput[], + componentLinkIndex: { + createdLinks: Map; + byComponentId: Map; + byRequirementId: Map; + }, + ) { + const links = Array.isArray(pieceLinks) ? pieceLinks : []; + if (links.length === 0) { + return new Map(); + } + + const pieceIds = new Set(); + const pendingEntries: PendingPieceLink[] = []; + + links.forEach((link, index) => { + const requirement = pieceRequirementMap.get(link.requirementId); + if (!requirement) { + throw new Error( + `Requirement de pièce introuvable (${link.requirementId}).`, + ); + } + + const pieceId = this.extractString(link.pieceId); + if (!pieceId) { + throw new Error( + `pieceId manquant pour le lien de pièce #${index + 1} (${this.describeRequirement(requirement)}).`, + ); + } + + pieceIds.add(pieceId); + + pendingEntries.push({ + raw: link, + assignedId: this.resolveLinkIdentifier(link) ?? randomUUID(), + requirement, + pieceId, + position: index, + }); + }); + + const pieces = await prisma.piece.findMany({ + where: { id: { in: Array.from(pieceIds) } }, include: { typePiece: true }, }); + const pieceMap = new Map( + pieces.map((piece) => [piece.id, piece]), + ); - if (!piece) { - throw new Error(`Pièce introuvable (${pieceId}).`); - } + for (const entry of pendingEntries) { + const piece = pieceMap.get(entry.pieceId); + if (!piece) { + throw new Error( + `Pièce introuvable (${entry.pieceId}) pour le lien de pièce #${entry.position + 1}.`, + ); + } - if ( - requirement.typePieceId && - piece.typePieceId && - piece.typePieceId !== requirement.typePieceId - ) { - throw new Error( - `La pièce sélectionnée (${piece.name || piece.id}) n'appartient pas à la famille attendue pour ce requirement.`, + if ( + entry.requirement.typePieceId && + piece.typePieceId && + piece.typePieceId !== entry.requirement.typePieceId + ) { + throw new Error( + `La pièce "${this.describePiece(piece)}" n'appartient pas à la famille attendue pour "${this.describeRequirement(entry.requirement)}".`, + ); + } + + entry.piece = piece; + entry.overrideMutation = this.buildLinkOverrideMutation( + this.ensurePlainObject(entry.raw.overrides), ); } - await this.prisma.piece.update({ - where: { id: piece.id }, - data: { + const createdLinks = new Map(); + + for (const entry of pendingEntries) { + const parentId = this.resolvePieceParentReference( + entry.raw, + componentLinkIndex, + ); + + const createData: Prisma.MachinePieceLinkUncheckedCreateInput = { + id: entry.assignedId, machineId, - composantId: null, - typeMachinePieceRequirementId: requirement.id, - }, - }); + pieceId: entry.pieceId, + parentLinkId: parentId ?? null, + typeMachinePieceRequirementId: entry.requirement.id, + }; + + if (entry.overrideMutation?.nameOverride !== undefined) { + createData.nameOverride = entry.overrideMutation.nameOverride; + } + if (entry.overrideMutation?.referenceOverride !== undefined) { + createData.referenceOverride = entry.overrideMutation.referenceOverride; + } + if (entry.overrideMutation?.prixOverride !== undefined) { + createData.prixOverride = entry.overrideMutation.prixOverride; + } + + await prisma.machinePieceLink.create({ data: createData }); + + createdLinks.set(entry.assignedId, { + id: entry.assignedId, + pieceId: entry.pieceId, + requirementId: entry.requirement.id, + parentLinkId: parentId ?? null, + }); + } + + return createdLinks; } - private async createComponentsForMachine( - machineId: string, - typeMachine: TypeMachineConfiguration, - selectionMap: Map, - ) { - const requirements = ( - Array.isArray(typeMachine.componentRequirements) - ? typeMachine.componentRequirements - : [] - ) as ComponentRequirementWithType[]; - - for (const requirement of requirements) { - const selections = selectionMap.get(requirement.id) ?? []; - for (const selection of selections) { - if (selection.composantId) { - await this.attachExistingComponentToMachine( - machineId, - requirement, - selection, - ); - continue; - } + private resolvePieceParentReference( + link: MachinePieceLinkInput, + componentLinkIndex: { + createdLinks: Map; + byComponentId: Map; + byRequirementId: Map; + }, + ): string | null { + const explicitParentId = this.extractString( + link.parentComponentLinkId ?? link.parentLinkId, + ); + if (explicitParentId) { + if (!componentLinkIndex.createdLinks.has(explicitParentId)) { throw new Error( - `Aucun composant existant fourni pour le requirement "${ - requirement.label || requirement.typeComposant?.name || requirement.id - }".`, + `Lien parent de composant introuvable (${explicitParentId}) pour une pièce sélectionnée.`, ); } + return explicitParentId; } - } - private async createPiecesForMachine( - machineId: string, - typeMachine: TypeMachineConfiguration, - selectionMap: Map, - ) { - const requirements = ( - Array.isArray(typeMachine.pieceRequirements) - ? typeMachine.pieceRequirements - : [] - ) as PieceRequirementWithType[]; - - for (const requirement of requirements) { - const selections = selectionMap.get(requirement.id) ?? []; - for (const selection of selections) { - if (selection.pieceId) { - await this.attachExistingPieceToMachine( - machineId, - requirement, - selection, - ); - continue; - } + const parentComponentRequirementId = this.extractString( + link.parentComponentRequirementId ?? + link.parentRequirementId ?? + link.parentMachineComponentRequirementId, + ); + if (parentComponentRequirementId) { + const matches = + componentLinkIndex.byRequirementId.get(parentComponentRequirementId) ?? []; + if (matches.length === 1) { + return matches[0].id; + } + if (matches.length > 1) { throw new Error( - `Aucune pièce existante fournie pour le requirement "${ - requirement.label || requirement.typePiece?.name || requirement.id - }".`, + `Plusieurs liens de composant correspondent au requirement parent (${parentComponentRequirementId}) pour une pièce.`, ); } + throw new Error( + `Lien de composant parent attendu pour le requirement ${parentComponentRequirementId} introuvable.`, + ); } + + const parentComponentId = this.extractString( + link.parentComponentId ?? link.composantId, + ); + if (parentComponentId) { + const matches = + componentLinkIndex.byComponentId.get(parentComponentId) ?? []; + if (matches.length === 1) { + return matches[0].id; + } + if (matches.length > 1) { + throw new Error( + `Impossible de déterminer le lien de composant parent pour ${parentComponentId}: plusieurs occurrences trouvées.`, + ); + } + throw new Error( + `Lien de composant parent attendu pour ${parentComponentId} introuvable.`, + ); + } + + return null; } private extractCustomFieldValue(field: any): string | undefined { @@ -826,8 +1656,8 @@ export class MachinesService { async create(createMachineDto: CreateMachineDto) { const { - componentSelections = [], - pieceSelections = [], + componentLinks = [], + pieceLinks = [], ...machineData } = createMachineDto; @@ -841,12 +1671,14 @@ export class MachinesService { machineData.typeMachineId, ); - const { componentSelectionMap, pieceSelectionMap } = - this.buildConfigurationContext( - typeMachine, - componentSelections, - pieceSelections, - ); + const { + componentRequirementMap, + pieceRequirementMap, + } = this.buildConfigurationContext( + typeMachine, + componentLinks, + pieceLinks, + ); const machine = await this.prisma.machine.create({ data: machineData, @@ -867,16 +1699,19 @@ export class MachinesService { ); } - await this.createComponentsForMachine( + const componentIndex = await this.createComponentLinksForMachine( + this.prisma, machine.id, - typeMachine, - componentSelectionMap, + componentRequirementMap, + componentLinks, ); - await this.createPiecesForMachine( + await this.createPieceLinksForMachine( + this.prisma, machine.id, - typeMachine, - pieceSelectionMap, + pieceRequirementMap, + pieceLinks, + componentIndex, ); } catch (error) { await this.prisma.machine @@ -911,8 +1746,7 @@ export class MachinesService { } async reconfigure(id: string, reconfigureMachineDto: ReconfigureMachineDto) { - const { componentSelections = [], pieceSelections = [] } = - reconfigureMachineDto; + const { componentLinks = [], pieceLinks = [] } = reconfigureMachineDto; const machine = await this.prisma.machine.findUnique({ where: { id }, @@ -935,62 +1769,33 @@ export class MachinesService { const typeMachine = machine.typeMachine as TypeMachineConfiguration; - const { componentSelectionMap, pieceSelectionMap } = + const { componentRequirementMap, pieceRequirementMap } = this.buildConfigurationContext( typeMachine, - componentSelections, - pieceSelections, + componentLinks, + pieceLinks, ); - await this.prisma.customFieldValue.deleteMany({ - where: { - OR: [ - { - composant: { - machineId: id, - typeMachineComponentRequirementId: { not: null }, - }, - }, - { - piece: { - machineId: id, - typeMachinePieceRequirementId: { not: null }, - }, - }, - { - piece: { - composant: { - machineId: id, - typeMachineComponentRequirementId: { not: null }, - }, - }, - }, - ], - }, + await this.prisma.$transaction(async (tx) => { + await tx.machinePieceLink.deleteMany({ where: { machineId: id } }); + await tx.machineComponentLink.deleteMany({ where: { machineId: id } }); + + const componentIndex = await this.createComponentLinksForMachine( + tx, + id, + componentRequirementMap, + componentLinks, + ); + + await this.createPieceLinksForMachine( + tx, + id, + pieceRequirementMap, + pieceLinks, + componentIndex, + ); }); - await this.prisma.piece.deleteMany({ - where: { - machineId: id, - typeMachinePieceRequirementId: { not: null }, - }, - }); - - await this.prisma.composant.deleteMany({ - where: { - machineId: id, - typeMachineComponentRequirementId: { not: null }, - }, - }); - - await this.createComponentsForMachine( - id, - typeMachine, - componentSelectionMap, - ); - - await this.createPiecesForMachine(id, typeMachine, pieceSelectionMap); - const updatedMachine = await this.prisma.machine.findUnique({ where: { id }, include: MACHINE_DEFAULT_INCLUDE, @@ -1094,355 +1899,33 @@ export class MachinesService { const machine = await this.prisma.machine.findUnique({ where: { id: machineId }, include: { - typeMachine: true, - composants: { - include: { - pieces: true, - }, + typeMachine: { + include: TYPE_MACHINE_CONFIGURATION_INCLUDE, }, - pieces: true, }, }); - if (!machine || !machine.typeMachine) { + if (!machine || !machine.typeMachineId || !machine.typeMachine) { throw new Error('Machine ou type de machine non trouvé'); } - const typeMachine = machine.typeMachine as any; - const components = typeMachine.components || []; - const machinePieces = typeMachine.machinePieces || []; - const machineCustomFields = typeMachine.customFields || []; + const typeMachine = machine.typeMachine as TypeMachineConfiguration & { + customFields?: any[]; + }; - if (machineCustomFields && machineCustomFields.length > 0) { - for (const customField of machineCustomFields) { - const existingValue = await this.prisma.customFieldValue.findFirst({ - where: { - machineId: machineId, - customField: { - name: customField.name, - typeMachineId: machine.typeMachineId, - }, - }, - }); + const machineCustomFields = Array.isArray(typeMachine.customFields) + ? typeMachine.customFields + : []; - if (!existingValue) { - let targetCustomField = await this.prisma.customField.findFirst({ - where: { - name: customField.name, - typeMachineId: machine.typeMachineId, - }, - }); - - if (!targetCustomField) { - targetCustomField = await this.prisma.customField.create({ - data: { - name: customField.name, - type: customField.type, - required: customField.required || false, - options: customField.options || [], - typeMachineId: machine.typeMachineId, - }, - }); - } - - const providedValue = this.extractCustomFieldValue(customField); - if (providedValue !== undefined) { - await this.prisma.customFieldValue.create({ - data: { - value: providedValue, - customFieldId: targetCustomField.id, - machineId, - }, - }); - } - } - } - } - - for (const component of machine.composants) { - const typeComponent = components.find( - (c: any) => c.name === component.name, + if (machineCustomFields.length > 0) { + await this.createMachineCustomFieldsFromType( + this.prisma, + machineId, + machineCustomFields, + machine.typeMachineId, ); - if ( - typeComponent && - typeComponent.customFields && - typeComponent.customFields.length > 0 - ) { - const typeComponentFields = Array.isArray(typeComponent.customFields) - ? typeComponent.customFields - : []; - const typeComponentFieldMap = new Map( - typeComponentFields - .filter((field: any) => field && typeof field.name === 'string') - .map((field: any) => [field.name, field]), - ); - - let typeComposant = await this.prisma.modelType.findFirst({ - where: { - name: component.name, - category: ModelCategory.COMPONENT, - }, - }); - - if (!typeComposant) { - typeComposant = await this.prisma.modelType.create({ - data: { - name: component.name, - code: await this.generateUniqueComponentTypeCode( - this.prisma, - component.name, - ), - category: ModelCategory.COMPONENT, - description: typeComponent.description || '', - }, - }); - } - - for (const customField of typeComponentFields) { - const existingField = await this.prisma.customField.findFirst({ - where: { - name: customField.name, - typeComposantId: typeComposant.id, - }, - }); - - if (!existingField) { - await this.prisma.customField.create({ - data: { - name: customField.name, - type: customField.type, - required: customField.required || false, - options: customField.options || [], - typeComposantId: typeComposant.id, - }, - }); - } - } - - await this.prisma.composant.update({ - where: { id: component.id }, - data: { typeComposantId: typeComposant.id }, - }); - - const customFields = await this.prisma.customField.findMany({ - where: { typeComposantId: typeComposant.id }, - }); - - for (const customField of customFields) { - const existingValue = await this.prisma.customFieldValue.findFirst({ - where: { - customFieldId: customField.id, - composantId: component.id, - }, - }); - - if (!existingValue) { - const providedValue = this.extractCustomFieldValue( - typeComponentFieldMap.get(customField.name), - ); - if (providedValue !== undefined) { - await this.prisma.customFieldValue.create({ - data: { - value: providedValue, - customFieldId: customField.id, - composantId: component.id, - }, - }); - } - } - } - - 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 typePieceFields = Array.isArray(typePiece.customFields) - ? typePiece.customFields - : []; - const typePieceFieldMap = new Map( - typePieceFields - .filter((field: any) => field && typeof field.name === 'string') - .map((field: any) => [field.name, field]), - ); - - let typePieceModel = await this.prisma.modelType.findFirst({ - where: { - name: piece.name, - category: ModelCategory.PIECE, - }, - }); - - if (!typePieceModel) { - typePieceModel = await this.prisma.modelType.create({ - data: { - name: piece.name, - code: await this.generateUniqueComponentTypeCode( - this.prisma, - piece.name, - ), - category: ModelCategory.PIECE, - description: typePiece.description || '', - }, - }); - } - - for (const customField of typePieceFields) { - const existingField = await this.prisma.customField.findFirst({ - where: { - name: customField.name, - typePieceId: typePieceModel.id, - }, - }); - - if (!existingField) { - await this.prisma.customField.create({ - data: { - name: customField.name, - type: customField.type, - required: customField.required || false, - options: customField.options || [], - typePieceId: typePieceModel.id, - }, - }); - } - } - - await this.prisma.piece.update({ - where: { id: piece.id }, - data: { typePieceId: typePieceModel.id }, - }); - - const pieceCustomFields = await this.prisma.customField.findMany({ - where: { typePieceId: typePieceModel.id }, - }); - - for (const customField of pieceCustomFields) { - const existingValue = - await this.prisma.customFieldValue.findFirst({ - where: { - customFieldId: customField.id, - pieceId: piece.id, - }, - }); - - if (!existingValue) { - const providedValue = this.extractCustomFieldValue( - typePieceFieldMap.get(customField.name), - ); - if (providedValue !== undefined) { - await this.prisma.customFieldValue.create({ - data: { - value: providedValue, - customFieldId: customField.id, - pieceId: piece.id, - }, - }); - } - } - } - } - } - } } - for (const piece of machine.pieces) { - const typePiece = machinePieces.find((p: any) => p.name === piece.name); - if ( - typePiece && - typePiece.customFields && - typePiece.customFields.length > 0 - ) { - const typePieceFields = Array.isArray(typePiece.customFields) - ? typePiece.customFields - : []; - const typePieceFieldMap = new Map( - typePieceFields - .filter((field: any) => field && typeof field.name === 'string') - .map((field: any) => [field.name, field]), - ); - - let typePieceModel = await this.prisma.modelType.findFirst({ - where: { - name: piece.name, - category: ModelCategory.PIECE, - }, - }); - - if (!typePieceModel) { - typePieceModel = await this.prisma.modelType.create({ - data: { - name: piece.name, - code: await this.generateUniqueComponentTypeCode( - this.prisma, - piece.name, - ), - category: ModelCategory.PIECE, - description: typePiece.description || '', - }, - }); - } - - for (const customField of typePieceFields) { - const existingField = await this.prisma.customField.findFirst({ - where: { - name: customField.name, - typePieceId: typePieceModel.id, - }, - }); - - if (!existingField) { - await this.prisma.customField.create({ - data: { - name: customField.name, - type: customField.type, - required: customField.required || false, - options: customField.options || [], - typePieceId: typePieceModel.id, - }, - }); - } - } - - await this.prisma.piece.update({ - where: { id: piece.id }, - data: { typePieceId: typePieceModel.id }, - }); - - const pieceCustomFields = await this.prisma.customField.findMany({ - where: { typePieceId: typePieceModel.id }, - }); - - for (const customField of pieceCustomFields) { - const existingValue = await this.prisma.customFieldValue.findFirst({ - where: { - customFieldId: customField.id, - pieceId: piece.id, - }, - }); - - if (!existingValue) { - const providedValue = this.extractCustomFieldValue( - typePieceFieldMap.get(customField.name), - ); - if (providedValue !== undefined) { - await this.prisma.customFieldValue.create({ - data: { - value: providedValue, - customFieldId: customField.id, - pieceId: piece.id, - }, - }); - } - } - } - } - } - - return this.findOne(machineId); + return { success: true }; } } diff --git a/src/pieces/pieces.controller.ts b/src/pieces/pieces.controller.ts index 25b65ff..147dd72 100644 --- a/src/pieces/pieces.controller.ts +++ b/src/pieces/pieces.controller.ts @@ -24,16 +24,6 @@ export class PiecesController { return this.piecesService.findAll(); } - @Get('machine/:machineId') - findByMachine(@Param('machineId') machineId: string) { - return this.piecesService.findByMachine(machineId); - } - - @Get('composant/:composantId') - findByComposant(@Param('composantId') composantId: string) { - return this.piecesService.findByComposant(composantId); - } - @Get(':id') findOne(@Param('id') id: string) { return this.piecesService.findOne(id); diff --git a/src/pieces/pieces.service.spec.ts b/src/pieces/pieces.service.spec.ts index e7d927e..8dcac4a 100644 --- a/src/pieces/pieces.service.spec.ts +++ b/src/pieces/pieces.service.spec.ts @@ -1,12 +1,11 @@ -import { BadRequestException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { PiecesService } from './pieces.service'; import { PrismaService } from '../prisma/prisma.service'; -import { CreatePieceDto } from '../shared/dto/piece.dto'; +import { CreatePieceDto, UpdatePieceDto } from '../shared/dto/piece.dto'; describe('PiecesService', () => { let service: PiecesService; - let prisma: any; + let prisma: { piece: any; customField: any; customFieldValue: any }; beforeEach(async () => { prisma = { @@ -17,12 +16,6 @@ describe('PiecesService', () => { update: jest.fn(), delete: jest.fn(), }, - machine: { - findUnique: jest.fn(), - }, - composant: { - findUnique: jest.fn(), - }, customField: { findMany: jest.fn(), create: jest.fn(), @@ -34,134 +27,42 @@ describe('PiecesService', () => { }; const module: TestingModule = await Test.createTestingModule({ - providers: [PiecesService, { provide: PrismaService, useValue: prisma }], + providers: [ + PiecesService, + { provide: PrismaService, useValue: prisma }, + ], }).compile(); service = module.get(PiecesService); }); - afterEach(() => { - jest.clearAllMocks(); - }); - - it('should create a piece when requirement matches the machine skeleton', async () => { + it('creates a piece', async () => { const dto: CreatePieceDto = { name: 'Piece A', - machineId: 'machine-1', typePieceId: 'type-piece-1', - typeMachinePieceRequirementId: 'req-1', }; - prisma.machine.findUnique.mockResolvedValue({ - id: 'machine-1', - typeMachine: { - pieceRequirements: [ - { - id: 'req-1', - typePieceId: 'type-piece-1', - typePiece: { - id: 'type-piece-1', - pieceSkeleton: { - customFields: [ - { - name: 'Numéro de série', - value: 'AUTO', - type: 'text', - required: true, - }, - ], - }, - }, - }, - ], - }, - }); - - const created = { - id: 'piece-1', - typePieceId: 'type-piece-1', - typePiece: { - id: 'type-piece-1', - pieceSkeleton: { - customFields: [ - { - name: 'Numéro de série', - value: 'AUTO', - type: 'text', - required: true, - }, - ], - }, - }, - }; - prisma.piece.create.mockResolvedValue(created); - - prisma.customField.findMany - .mockResolvedValueOnce([]) - .mockResolvedValueOnce([{ id: 'field-1', name: 'Numéro de série' }]); - prisma.customField.create.mockResolvedValue({ id: 'field-1' }); + prisma.piece.create.mockResolvedValue({ id: 'piece-1', name: dto.name }); + prisma.piece.findUnique.mockResolvedValue({ id: 'piece-1', name: dto.name }); + prisma.customField.findMany.mockResolvedValue([]); prisma.customFieldValue.findMany.mockResolvedValue([]); - prisma.customFieldValue.create.mockResolvedValue({ - id: 'value-1', - }); - const finalPiece = { ...created, customFieldValues: [] }; - prisma.piece.findUnique.mockResolvedValue(finalPiece); + const result = await service.create(dto); - await expect(service.create(dto)).resolves.toEqual(finalPiece); - - expect(prisma.piece.create).toHaveBeenCalledWith({ - data: expect.objectContaining({ - machineId: 'machine-1', - typePieceId: 'type-piece-1', - }), - include: expect.any(Object), - }); - - expect(prisma.customField.create).toHaveBeenCalledWith({ - data: { - name: 'Numéro de série', - type: 'text', - required: true, - options: undefined, - typePieceId: 'type-piece-1', - }, - select: { id: true }, - }); - - expect(prisma.customFieldValue.create).toHaveBeenCalledWith({ - data: { - customFieldId: 'field-1', - pieceId: 'piece-1', - value: 'AUTO', - }, - }); - - expect(prisma.piece.findUnique).toHaveBeenCalledWith({ - where: { id: 'piece-1' }, - include: expect.any(Object), - }); + expect(prisma.piece.create).toHaveBeenCalled(); + expect(result).toMatchObject({ id: 'piece-1' }); }); - it('should refuse creation when requirement does not belong to machine skeleton', async () => { - const dto: CreatePieceDto = { - name: 'Piece A', - machineId: 'machine-1', - typePieceId: 'type-piece-1', - typeMachinePieceRequirementId: 'req-2', - }; + it('updates a piece', async () => { + const dto: UpdatePieceDto = { name: 'Updated piece' }; - prisma.machine.findUnique.mockResolvedValue({ - id: 'machine-1', - typeMachine: { - pieceRequirements: [{ id: 'req-1', typePieceId: 'type-piece-1' }], - }, - }); + prisma.piece.update.mockResolvedValue({ id: 'piece-1', name: 'Updated piece' }); + prisma.piece.findUnique.mockResolvedValue({ id: 'piece-1', name: 'Updated piece' }); + prisma.customField.findMany.mockResolvedValue([]); + prisma.customFieldValue.findMany.mockResolvedValue([]); - await expect(service.create(dto)).rejects.toBeInstanceOf( - BadRequestException, - ); + await service.update('piece-1', dto); - expect(prisma.piece.create).not.toHaveBeenCalled(); + expect(prisma.piece.update).toHaveBeenCalled(); }); }); diff --git a/src/pieces/pieces.service.ts b/src/pieces/pieces.service.ts index cfea433..3a24d3a 100644 --- a/src/pieces/pieces.service.ts +++ b/src/pieces/pieces.service.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { Prisma } from '@prisma/client'; import { PrismaService } from '../prisma/prisma.service'; import { CreatePieceDto, UpdatePieceDto } from '../shared/dto/piece.dto'; @@ -6,124 +6,56 @@ import { PieceModelStructureSchema } from '../shared/schemas/inventory'; import type { PieceModelStructure } from '../shared/types/inventory'; const PIECE_WITH_RELATIONS_INCLUDE = { - machine: true, - composant: true, typePiece: { include: { pieceCustomFields: true, }, }, - documents: true, constructeur: true, - typeMachinePieceRequirement: { - include: { - typePiece: { - include: { - pieceCustomFields: 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) {} - async create(createPieceDto: CreatePieceDto) { - const requirementId = createPieceDto.typeMachinePieceRequirementId ?? null; - - if (requirementId && !createPieceDto.machineId) { - throw new BadRequestException( - 'Un requirement ne peut pas être utilisé sans machine ciblée.', - ); - } - - let machineId = createPieceDto.machineId ?? null; - - if (createPieceDto.composantId) { - const composantMachineId = await this.resolveMachineIdFromComposant( - createPieceDto.composantId, - ); - - if (machineId && machineId !== composantMachineId) { - throw new BadRequestException( - 'Le composant ciblé appartient à une autre machine que celle fournie.', - ); - } - - machineId = composantMachineId ?? machineId; - } - - let requirement: PieceRequirementWithType | null = null; - - if (machineId) { - const machine = await this.prisma.machine.findUnique({ - where: { id: machineId }, - include: { - typeMachine: { - include: { - pieceRequirements: { - include: { - typePiece: true, - }, - }, - }, - }, - }, - }); - - if (!machine || !machine.typeMachine) { - throw new BadRequestException( - 'La machine ciblée doit être associée à un type de machine pour valider les requirements.', - ); - } - - if (requirementId) { - requirement = - ( - machine.typeMachine.pieceRequirements as PieceRequirementWithType[] - ).find((pieceRequirement) => pieceRequirement.id === requirementId) ?? - null; - - if (!requirement) { - throw new BadRequestException( - 'Le requirement de pièce fourni ne correspond pas au squelette de la machine.', - ); - } - - if ( - createPieceDto.typePieceId && - createPieceDto.typePieceId !== requirement.typePieceId - ) { - throw new BadRequestException( - 'Le type de pièce fourni ne correspond pas au requirement pour cette machine.', - ); - } - } - } - - const typePieceId = - createPieceDto.typePieceId ?? requirement?.typePieceId ?? null; - - const data: Prisma.PieceUncheckedCreateInput = { + private buildCreateInput(createPieceDto: CreatePieceDto): Prisma.PieceCreateInput { + const data: Prisma.PieceCreateInput = { name: createPieceDto.name, reference: createPieceDto.reference ?? null, - constructeurId: createPieceDto.constructeurId ?? null, prix: createPieceDto.prix !== undefined ? createPieceDto.prix : null, - machineId, - composantId: createPieceDto.composantId ?? null, - typePieceId, - typeMachinePieceRequirementId: requirement?.id ?? requirementId ?? 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) { const created = await this.prisma.piece.create({ - data, + data: this.buildCreateInput(createPieceDto), include: PIECE_WITH_RELATIONS_INCLUDE, }); @@ -141,6 +73,7 @@ export class PiecesService { async findAll() { return this.prisma.piece.findMany({ include: PIECE_WITH_RELATIONS_INCLUDE, + orderBy: { name: 'asc' }, }); } @@ -151,53 +84,36 @@ export class PiecesService { }); } - async findByMachine(machineId: string) { - return this.prisma.piece.findMany({ - where: { machineId }, - include: PIECE_WITH_RELATIONS_INCLUDE, - }); - } - - private async resolveMachineIdFromComposant( - composantId: string, - ): Promise { - const composant = await this.prisma.composant.findUnique({ - where: { id: composantId }, - select: { - id: true, - machineId: true, - parentComposantId: true, - }, - }); - - if (!composant) { - throw new BadRequestException('Le composant spécifié est introuvable.'); - } - - if (composant.machineId) { - return composant.machineId; - } - - if (composant.parentComposantId) { - return this.resolveMachineIdFromComposant(composant.parentComposantId); - } - - throw new BadRequestException( - 'Impossible de déterminer la machine associée à ce composant.', - ); - } - - async findByComposant(composantId: string) { - return this.prisma.piece.findMany({ - where: { composantId }, - 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 }; + } + const updated = await this.prisma.piece.update({ where: { id }, - data: updatePieceDto, + data, include: PIECE_WITH_RELATIONS_INCLUDE, }); @@ -241,7 +157,6 @@ export class PiecesService { const customFields = skeleton.customFields ?? []; await this.ensurePieceCustomFieldDefinitions(typePiece.id, customFields); - await this.createPieceCustomFieldValues( pieceId, typePiece.id, @@ -291,11 +206,7 @@ export class PiecesService { } const name = this.normalizeIdentifier(field.name); - if (!name) { - continue; - } - - if (existingByName.has(name)) { + if (!name || existingByName.has(name)) { continue; } @@ -426,16 +337,16 @@ export class PiecesService { return ''; } - return String(value); + if (typeof value === 'string') { + return value; + } + + return JSON.stringify(value); } } -type PieceRequirementWithType = Prisma.TypeMachinePieceRequirementGetPayload<{ - include: { typePiece: true }; +type PieceTypeWithSkeleton = Prisma.ModelTypeGetPayload<{ + include: { pieceCustomFields: true }; }>; -type PieceTypeWithSkeleton = PieceRequirementWithType['typePiece']; - -type PieceCustomFieldEntry = NonNullable< - PieceModelStructure['customFields'] ->[number]; +type PieceCustomFieldEntry = NonNullable[number]; diff --git a/src/shared/dto/composant.dto.ts b/src/shared/dto/composant.dto.ts index c842868..6a841a5 100644 --- a/src/shared/dto/composant.dto.ts +++ b/src/shared/dto/composant.dto.ts @@ -1,4 +1,4 @@ -import { IsString, IsOptional, IsNumber } from 'class-validator'; +import { IsString, IsOptional, IsNumber, IsObject } from 'class-validator'; import { Transform } from 'class-transformer'; export class CreateComposantDto { @@ -33,6 +33,10 @@ export class CreateComposantDto { @IsOptional() @IsString() typeMachineComponentRequirementId?: string; + + @IsOptional() + @IsObject() + structure?: Record; } export class UpdateComposantDto { @@ -56,4 +60,8 @@ export class UpdateComposantDto { @IsOptional() @IsString() typeComposantId?: string; + + @IsOptional() + @IsObject() + structure?: Record; } diff --git a/src/shared/dto/machine.dto.ts b/src/shared/dto/machine.dto.ts index 99c58e2..cee14cc 100644 --- a/src/shared/dto/machine.dto.ts +++ b/src/shared/dto/machine.dto.ts @@ -1,8 +1,22 @@ -import { IsString, IsOptional, IsDecimal, IsArray } from 'class-validator'; +import { + IsString, + IsOptional, + IsDecimal, + IsArray, + IsObject, +} from 'class-validator'; import { Type } from 'class-transformer'; import { ValidateNested } from 'class-validator'; -export class MachineComponentSelectionDto { +export class MachineComponentLinkPayloadDto { + @IsOptional() + @IsString() + id?: string; + + @IsOptional() + @IsString() + linkId?: string; + @IsString() requirementId: string; @@ -15,10 +29,59 @@ export class MachineComponentSelectionDto { composantId?: string; @IsOptional() - definition?: any; + @IsString() + componentId?: string; + + @IsOptional() + @IsString() + parentLinkId?: string; + + @IsOptional() + @IsString() + parentComponentLinkId?: string; + + @IsOptional() + @IsString() + parentRequirementId?: string; + + @IsOptional() + @IsString() + parentComponentRequirementId?: string; + + @IsOptional() + @IsString() + parentPieceRequirementId?: string; + + @IsOptional() + @IsString() + parentMachineComponentRequirementId?: string; + + @IsOptional() + @IsString() + parentMachinePieceRequirementId?: string; + + @IsOptional() + @IsString() + parentComponentId?: string; + + @IsOptional() + @IsString() + parentPieceId?: string; + + @IsOptional() + @IsObject() + overrides?: Record; } -export class MachinePieceSelectionDto { +export class MachinePieceLinkPayloadDto { + @IsOptional() + @IsString() + id?: string; + + @IsOptional() + @IsString() + linkId?: string; + @IsString() requirementId: string; @@ -31,7 +94,52 @@ export class MachinePieceSelectionDto { pieceId?: string; @IsOptional() - definition?: any; + @IsString() + composantId?: string; + + @IsOptional() + @IsString() + parentLinkId?: string; + + @IsOptional() + @IsString() + parentComponentLinkId?: string; + + @IsOptional() + @IsString() + parentPieceLinkId?: string; + + @IsOptional() + @IsString() + parentRequirementId?: string; + + @IsOptional() + @IsString() + parentComponentRequirementId?: string; + + @IsOptional() + @IsString() + parentPieceRequirementId?: string; + + @IsOptional() + @IsString() + parentMachineComponentRequirementId?: string; + + @IsOptional() + @IsString() + parentMachinePieceRequirementId?: string; + + @IsOptional() + @IsString() + parentComponentId?: string; + + @IsOptional() + @IsString() + parentPieceId?: string; + + @IsOptional() + @IsObject() + overrides?: Record; } export class CreateMachineDto { @@ -60,14 +168,14 @@ export class CreateMachineDto { @IsOptional() @IsArray() @ValidateNested({ each: true }) - @Type(() => MachineComponentSelectionDto) - componentSelections?: MachineComponentSelectionDto[]; + @Type(() => MachineComponentLinkPayloadDto) + componentLinks?: MachineComponentLinkPayloadDto[]; @IsOptional() @IsArray() @ValidateNested({ each: true }) - @Type(() => MachinePieceSelectionDto) - pieceSelections?: MachinePieceSelectionDto[]; + @Type(() => MachinePieceLinkPayloadDto) + pieceLinks?: MachinePieceLinkPayloadDto[]; } export class UpdateMachineDto { @@ -96,12 +204,15 @@ export class ReconfigureMachineDto { @IsOptional() @IsArray() @ValidateNested({ each: true }) - @Type(() => MachineComponentSelectionDto) - componentSelections?: MachineComponentSelectionDto[]; + @Type(() => MachineComponentLinkPayloadDto) + componentLinks?: MachineComponentLinkPayloadDto[]; @IsOptional() @IsArray() @ValidateNested({ each: true }) - @Type(() => MachinePieceSelectionDto) - pieceSelections?: MachinePieceSelectionDto[]; + @Type(() => MachinePieceLinkPayloadDto) + pieceLinks?: MachinePieceLinkPayloadDto[]; } + +export type MachineComponentLinkInput = MachineComponentLinkPayloadDto; +export type MachinePieceLinkInput = MachinePieceLinkPayloadDto; diff --git a/src/sites/sites.service.ts b/src/sites/sites.service.ts index fa71db5..32d360c 100644 --- a/src/sites/sites.service.ts +++ b/src/sites/sites.service.ts @@ -18,14 +18,8 @@ export class SitesService { machines: { include: { typeMachine: true, - composants: { - include: { - typeComposant: true, - sousComposants: true, - pieces: true, - }, - }, - pieces: true, + componentLinks: true, + pieceLinks: true, }, }, documents: true, @@ -40,14 +34,8 @@ export class SitesService { machines: { include: { typeMachine: true, - composants: { - include: { - typeComposant: true, - sousComposants: true, - pieces: true, - }, - }, - pieces: true, + componentLinks: true, + pieceLinks: true, }, }, documents: true, diff --git a/test/app.e2e-spec.ts b/test/app.e2e-spec.ts index 55e50b1..d5c6e9a 100644 --- a/test/app.e2e-spec.ts +++ b/test/app.e2e-spec.ts @@ -109,6 +109,32 @@ type PieceRecord = { updatedAt: Date; }; +type MachineComponentLinkRecord = { + id: string; + machineId: string; + composantId: string; + parentLinkId: Nullable; + typeMachineComponentRequirementId: Nullable; + nameOverride: Nullable; + referenceOverride: Nullable; + prixOverride: Nullable; + createdAt: Date; + updatedAt: Date; +}; + +type MachinePieceLinkRecord = { + id: string; + machineId: string; + pieceId: string; + parentLinkId: Nullable; + typeMachinePieceRequirementId: Nullable; + nameOverride: Nullable; + referenceOverride: Nullable; + prixOverride: Nullable; + createdAt: Date; + updatedAt: Date; +}; + type ModelTypeRecord = { id: string; name: string; @@ -171,6 +197,8 @@ class InMemoryPrismaService { private machines: MachineRecord[] = []; private composants: ComposantRecord[] = []; private pieces: PieceRecord[] = []; + private machineComponentLinks: MachineComponentLinkRecord[] = []; + private machinePieceLinks: MachinePieceLinkRecord[] = []; private customFields: CustomFieldRecord[] = []; private customFieldValues: CustomFieldValueRecord[] = []; private profiles: ProfileRecord[] = []; @@ -196,6 +224,8 @@ class InMemoryPrismaService { this.machines = []; this.composants = []; this.pieces = []; + this.machineComponentLinks = []; + this.machinePieceLinks = []; this.customFields = []; this.customFieldValues = []; this.profiles = []; @@ -720,6 +750,108 @@ class InMemoryPrismaService { }, }; + machineComponentLink = { + create: async ({ data, include }: any) => { + const now = new Date(); + const record: MachineComponentLinkRecord = { + id: data.id ?? generateId('mcl'), + machineId: data.machineId, + composantId: data.composantId, + parentLinkId: data.parentLinkId ?? null, + typeMachineComponentRequirementId: + data.typeMachineComponentRequirementId ?? null, + nameOverride: + data.nameOverride !== undefined ? data.nameOverride : null, + referenceOverride: + data.referenceOverride !== undefined ? data.referenceOverride : null, + prixOverride: + data.prixOverride !== undefined && data.prixOverride !== null + ? String(data.prixOverride) + : data.prixOverride ?? null, + createdAt: now, + updatedAt: now, + }; + this.machineComponentLinks.push(record); + return this.buildMachineComponentLink(record, include ?? {}); + }, + deleteMany: async ({ where }: any) => { + const before = this.machineComponentLinks.length; + this.machineComponentLinks = this.machineComponentLinks.filter((link) => { + if (where?.machineId !== undefined) { + return link.machineId !== where.machineId; + } + return true; + }); + return { count: before - this.machineComponentLinks.length }; + }, + findMany: async ({ where, include }: any = {}) => { + let links = this.machineComponentLinks; + if (where?.machineId) { + links = links.filter((link) => link.machineId === where.machineId); + } + if (where?.parentLinkId === null) { + links = links.filter((link) => link.parentLinkId === null); + } else if (where?.parentLinkId) { + links = links.filter( + (link) => link.parentLinkId === where.parentLinkId, + ); + } + return links.map((link) => + this.buildMachineComponentLink(link, include ?? {}), + ); + }, + }; + + machinePieceLink = { + create: async ({ data, include }: any) => { + const now = new Date(); + const record: MachinePieceLinkRecord = { + id: data.id ?? generateId('mpl'), + machineId: data.machineId, + pieceId: data.pieceId, + parentLinkId: data.parentLinkId ?? null, + typeMachinePieceRequirementId: + data.typeMachinePieceRequirementId ?? null, + nameOverride: + data.nameOverride !== undefined ? data.nameOverride : null, + referenceOverride: + data.referenceOverride !== undefined ? data.referenceOverride : null, + prixOverride: + data.prixOverride !== undefined && data.prixOverride !== null + ? String(data.prixOverride) + : data.prixOverride ?? null, + createdAt: now, + updatedAt: now, + }; + this.machinePieceLinks.push(record); + return this.buildMachinePieceLink(record, include ?? {}); + }, + deleteMany: async ({ where }: any) => { + const before = this.machinePieceLinks.length; + this.machinePieceLinks = this.machinePieceLinks.filter((link) => { + if (where?.machineId !== undefined) { + return link.machineId !== where.machineId; + } + return true; + }); + return { count: before - this.machinePieceLinks.length }; + }, + findMany: async ({ where, include }: any = {}) => { + let links = this.machinePieceLinks; + if (where?.machineId) { + links = links.filter((link) => link.machineId === where.machineId); + } + if (where?.parentLinkId === null) { + links = links.filter((link) => link.parentLinkId === null); + } else if (where?.parentLinkId) { + links = links.filter( + (link) => link.parentLinkId === where.parentLinkId, + ); + } + return links.map((link) => this.buildMachinePieceLink(link, include ?? {})); + }, + }; + customField = { create: async ({ data }: any) => { const now = new Date(); @@ -1181,6 +1313,27 @@ class InMemoryPrismaService { base.constructeur = null; } + if (include?.componentLinks) { + const links = this.machineComponentLinks.filter( + (link) => link.machineId === machine.id, + ); + base.componentLinks = links.map((link) => + this.buildMachineComponentLink( + link, + include.componentLinks.include ?? {}, + ), + ); + } + + if (include?.pieceLinks) { + const links = this.machinePieceLinks.filter( + (link) => link.machineId === machine.id, + ); + base.pieceLinks = links.map((link) => + this.buildMachinePieceLink(link, include.pieceLinks.include ?? {}), + ); + } + if (include?.composants) { const composants = this.composants.filter( (component) => component.machineId === machine.id, @@ -1220,6 +1373,87 @@ class InMemoryPrismaService { return base; } + private buildMachineComponentLink( + link: MachineComponentLinkRecord, + include: any, + ) { + const base: any = { ...link }; + + if (include?.composant) { + const composant = + this.composants.find((item) => item.id === link.composantId) ?? null; + base.composant = composant + ? this.buildComponent(composant, include.composant.include ?? {}) + : null; + } + + if (include?.typeMachineComponentRequirement) { + const requirement = link.typeMachineComponentRequirementId + ? this.typeMachineComponentRequirements.find( + (item) => item.id === link.typeMachineComponentRequirementId, + ) ?? null + : null; + base.typeMachineComponentRequirement = requirement + ? { + ...requirement, + typeComposant: + include.typeMachineComponentRequirement.include?.typeComposant + ? this.typeComposants.find( + (item) => item.id === requirement.typeComposantId, + ) ?? null + : undefined, + } + : null; + } + + if (include?.pieceLinks) { + const nestedInclude = include.pieceLinks.include ?? {}; + const pieces = this.machinePieceLinks.filter( + (pieceLink) => pieceLink.parentLinkId === link.id, + ); + base.pieceLinks = pieces.map((pieceLink) => + this.buildMachinePieceLink(pieceLink, nestedInclude), + ); + } + + return base; + } + + private buildMachinePieceLink( + link: MachinePieceLinkRecord, + include: any, + ) { + const base: any = { ...link }; + + if (include?.piece) { + const piece = this.pieces.find((item) => item.id === link.pieceId) ?? null; + base.piece = piece + ? this.buildPiece(piece, include.piece.include ?? {}) + : null; + } + + if (include?.typeMachinePieceRequirement) { + const requirement = link.typeMachinePieceRequirementId + ? this.typeMachinePieceRequirements.find( + (item) => item.id === link.typeMachinePieceRequirementId, + ) ?? null + : null; + base.typeMachinePieceRequirement = requirement + ? { + ...requirement, + typePiece: + include.typeMachinePieceRequirement.include?.typePiece + ? this.typePieces.find( + (item) => item.id === requirement.typePieceId, + ) ?? null + : undefined, + } + : null; + } + + return base; + } + private buildComponent(component: ComposantRecord, include: any) { const base: any = { ...component }; @@ -1459,42 +1693,46 @@ describe('Inventory flow (e2e)', () => { const componentRequirementId = typeMachine.componentRequirements[0].id; const pieceRequirementId = typeMachine.pieceRequirements[0].id; + const baseComponent = await prisma.composant.create({ + data: { + name: 'Bloc moteur standard', + reference: 'COMP-BASE', + typeComposantId, + }, + }); + + const basePiece = await prisma.piece.create({ + data: { + name: 'Kit maintenance standard', + reference: 'KIT-BASE', + typePieceId, + }, + }); + const machineResponse = await request(app.getHttpServer()) .post('/machines') .send({ name: 'Presse HP-2000', siteId, typeMachineId: typeMachine.id, - componentSelections: [ + componentLinks: [ { requirementId: componentRequirementId, - definition: { + composantId: baseComponent.id, + overrides: { name: 'Bloc moteur série X', reference: 'COMP-001', - customFields: [ - { - name: 'Puissance nominale', - type: 'text', - required: true, - value: '7 kW', - }, - ], + prix: '12000.00', }, }, ], - pieceSelections: [ + pieceLinks: [ { requirementId: pieceRequirementId, - definition: { + pieceId: basePiece.id, + overrides: { name: 'Kit maintenance niveau 1', reference: 'KIT-001', - customFields: [ - { - name: 'Référence fournisseur', - type: 'text', - value: 'STD-002', - }, - ], }, }, ], @@ -1509,31 +1747,19 @@ describe('Inventory flow (e2e)', () => { expect(machineDetailsResponse.status).toBe(200); const machine = machineDetailsResponse.body; - expect(machine.composants).toHaveLength(1); - expect(machine.pieces).toHaveLength(1); + expect(machine.componentLinks).toHaveLength(1); + expect(machine.pieceLinks).toHaveLength(1); - const component = machine.composants[0]; - expect(component.name).toBe('Bloc moteur série X'); - expect(component.customFieldValues[0].value).toBe('7 kW'); + const componentLink = machine.componentLinks[0]; + expect(componentLink.composantId).toBe(baseComponent.id); + expect(componentLink.overrides.name).toBe('Bloc moteur série X'); + expect(componentLink.composant.name).toBe('Bloc moteur série X'); + expect(componentLink.originalComposant.name).toBe('Bloc moteur standard'); - const piece = machine.pieces[0]; - expect(piece.name).toBe('Kit maintenance niveau 1'); - expect(piece.customFieldValues[0].value).toBe('STD-002'); - - const customFieldValueId = component.customFieldValues[0].id; - const updateResponse = await request(app.getHttpServer()) - .patch(`/custom-fields/values/${customFieldValueId}`) - .send({ value: '8 kW' }); - - expect(updateResponse.status).toBe(200); - expect(updateResponse.body.value).toBe('8 kW'); - - 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'); + const pieceLink = machine.pieceLinks[0]; + expect(pieceLink.pieceId).toBe(basePiece.id); + expect(pieceLink.overrides.name).toBe('Kit maintenance niveau 1'); + expect(pieceLink.piece.name).toBe('Kit maintenance niveau 1'); }); describe('POST /composants', () => {