diff --git a/src/management-system-v2/components/process-import.tsx b/src/management-system-v2/components/process-import.tsx index e55fc28da..40d3b531e 100644 --- a/src/management-system-v2/components/process-import.tsx +++ b/src/management-system-v2/components/process-import.tsx @@ -1,20 +1,30 @@ 'use client'; -import React, { useState } from 'react'; +import React, { useState, use } from 'react'; import { Button, Upload } from 'antd'; import type { ButtonProps } from 'antd'; import { - getDefinitionsId, + generateBpmnId, getDefinitionsName, getProcessDocumentation, + initXml, + setProceedElement, toBpmnObject, } from '@proceed/bpmn-helper'; import ProcessModal from './process-modal'; import { addProcesses } from '@/lib/data/processes'; import { useRouter } from 'next/navigation'; import { useEnvironment } from './auth-can'; +import BpmnModeler from 'bpmn-js/lib/Modeler'; +import type { ShapeLike } from 'diagram-js/lib/core/Types'; +import type ElementRegistry from 'diagram-js/lib/core/ElementRegistry'; +import type ElementFactory from 'diagram-js/lib/core/ElementFactory'; +import type Canvas from 'diagram-js/lib/core/Canvas'; +import type BpmnFactory from 'bpmn-js/lib/features/modeling/BpmnFactory'; +import type Modeling from 'bpmn-js/lib/features/modeling/Modeling'; +import { EnvVarsContext } from './env-vars-context'; export type ProcessData = { name: string; @@ -22,24 +32,357 @@ export type ProcessData = { bpmn: string; }; +type JSONProcessSchema = { + work_plan: { + product_name: string; + product_id: string; + process: JSONProcessTaskSchema[]; + }; + features: { + [key: string]: { + general: { + bore_length: number; + bore_radius: number; + id: number; + type: string; + }; + geometry: { + [key: string]: { + geometry: { + angle: number; + area: number; + 'face type': string; + length: number; + radius: number; + stepEntity: string; + stepID: number; + }; + tolerances: { + actualHorizontal: { + Ra: number; + Rk: number; + Rpk: number; + Rt: number; + Rvk: number; + Rz: number; + }; + actualVertical: { + Ra: number; + Rk: number; + Rpk: number; + Rt: number; + Rvk: number; + Rz: number; + }; + targetHorizontal: { + Ra: number; + Rk: number; + Rpk: number; + Rt: number; + Rvk: number; + Rz: number; + }; + targetVertical: { + Ra: number; + Rk: number; + Rpk: number; + Rt: number; + Rvk: number; + Rz: number; + }; + }; + }; + }; + }; + }; + production_facilities: { + production_facilities: Array<{ + production_facility: string; + process: Array<{ + process_position_id: number; + production_process: string; + description: string; + parameter: { + when: string; + manual_automated: string; + 'machine hour rate [€/h]': string; + }; + result: string; + }>; + }>; + }; +}; + +type JSONProcessTaskSchema = { + process_id: number; + process_name: string; + process_position_id: number; + production_process: string; + production_facility: { + [key: string]: { + facility_reference: string; + 'total processing time [min]': string; + 'total processing costs [€]': string; + parameter: Array<{ + facility_reference: 'production_facilities.Chisel_system'; + 'total processing time [min]': ''; + 'total processing costs [€]': ''; + parameter: Array<{ + Schritt: number; + 'Zeit von [min]': number; + 'Zeit bis [min]': number; + Werkzeug: string; + 'v_w [m/s]': number; + 'z_w [mm]': number; + 'f_a [Hz]': number; + 'Ra von': number; + 'Ra bis': number; + }>; + }>; + }; + }; + results: Array<{ + feature_name: string; + face_ID: { + [key: string]: { geometry: string }; + }; + general: string; + }>; +}; + // TODO: maybe show import errors and warnings like in the old MS (e.g. id collisions if an existing process is reimported or two imports use the same id) const ProcessImportButton: React.FC = ({ ...props }) => { + const env = use(EnvVarsContext); const [importProcessData, setImportProcessData] = useState([]); const router = useRouter(); const environment = useEnvironment(); + const createTaskShape = ( + bpmnModeler: BpmnModeler, + taskInfo: JSONProcessTaskSchema, + processInfo: JSONProcessSchema, + taskPosition: { x: number; y: number }, + ) => { + const modeling = bpmnModeler.get('modeling') as Modeling; + const elementFactory = bpmnModeler.get('elementFactory') as ElementFactory; + const bpmnFactory = bpmnModeler.get('bpmnFactory') as BpmnFactory; + const rootElement = (bpmnModeler.get('canvas') as Canvas).getRootElement(); + + let taskDescription = + '| feature_name | feature_id | face_ID |' + '\n' + '| ------------ | ---------- | ------- |'; + + taskInfo.results.forEach((result: any) => { + taskDescription += + '\n' + `| ${result.feature_name} | ${result.feature_id} | ${result.face_ID.join(', ')} |`; + }); + + const facility = processInfo.production_facilities.production_facilities.find( + (entry) => entry.production_facility === Object.keys(taskInfo.production_facility)[0], + ); + const isUserTask = facility?.process; + const taskShape = modeling.createShape( + elementFactory.createShape({ + type: isUserTask ? 'bpmn:UserTask' : 'bpmn:ServiceTask', + }), + taskPosition, + rootElement as any, + ); + taskShape.businessObject.name = taskInfo.process_name + .replace(/_/g, ' ') + .replace(/^./, (char) => char.toUpperCase()); + taskShape.businessObject.documentation = [ + bpmnFactory.create('bpmn:Documentation', { + text: taskDescription, + }), + ]; + setProceedElement(taskShape.businessObject, 'property', taskInfo.production_facility[0], { + name: 'production_facility', + }); + setProceedElement(taskShape.businessObject, 'property', taskInfo.production_process, { + name: 'production_process', + }); + + return taskShape; + }; + + const importJsonProcess = async (json: string) => { + console.log('importJsonProcess', json); + const processInfo = JSON.parse(json) as JSONProcessSchema; + + const sortedTasks = processInfo.work_plan.process + .sort((taskA, taskB) => taskA.process_position_id - taskB.process_position_id) + .reduce<{ positionID: number; tasks: JSONProcessTaskSchema[] }[]>((acc, curr) => { + const isAlreadyAdded = acc.find((t) => t.positionID === curr.process_position_id); + if (isAlreadyAdded) { + return acc; + } + + const parallelTasks = processInfo.work_plan.process.filter( + (t) => t.process_position_ID === curr.process_position_id, + ); + + return [...acc, { positionID: curr.process_position_id, tasks: parallelTasks }]; + }, []); + + const processId = `Process_${generateBpmnId()}`; + const startEventId = `StartEvent_${generateBpmnId()}`; + const bpmn = initXml(processId, startEventId); + + const bpmnModeler = new BpmnModeler(); + await bpmnModeler.importXML(bpmn); + + const modeling = bpmnModeler.get('modeling') as Modeling; + const elementRegistry = bpmnModeler.get('elementRegistry') as ElementRegistry; + const elementFactory = bpmnModeler.get('elementFactory') as ElementFactory; + const rootElement = (bpmnModeler.get('canvas') as Canvas).getRootElement() as any; + + setProceedElement(rootElement.businessObject, 'property', processInfo.work_plan.product_name, { + name: 'product_name', + }); + + const startEvent = elementRegistry.get(startEventId)! as ShapeLike; + const yPosition = startEvent.y + startEvent.height / 2; + const xPositionOffset = 150; + const yPositionOffset = 150; + + sortedTasks.reduce<{ + createdShapes: ShapeLike[]; + currentXPosition: number; + }>( + ({ createdShapes, currentXPosition }, { tasks: currentPositionIDTasks }, sortedTaskIdx) => { + const newShapes: ShapeLike[] = []; + let newCurrentXPosition = currentXPosition; + + if (currentPositionIDTasks.length > 1) { + // Create Parallel Gateway for parallel Tasks with same position ID + const parallelGatewayOutgoing = modeling.createShape( + elementFactory.createShape({ + type: 'bpmn:ParallelGateway', + }), + { x: (newCurrentXPosition += xPositionOffset), y: yPosition }, + rootElement, + ); + + const minYPosition = + currentPositionIDTasks.length % 2 === 0 + ? yPosition + + yPositionOffset / 2 - + (currentPositionIDTasks.length / 2) * yPositionOffset + : yPosition - Math.floor(currentPositionIDTasks.length / 2) * yPositionOffset; + + const parallelTaskShapes: ShapeLike[] = []; + newCurrentXPosition += xPositionOffset; + + currentPositionIDTasks.forEach((task, index) => { + const parallelTaskShape = createTaskShape(bpmnModeler, task, { + x: newCurrentXPosition, + y: minYPosition + yPositionOffset * index, + }); + modeling.createConnection( + parallelGatewayOutgoing, + parallelTaskShape, + { + type: 'bpmn:SequenceFlow', + }, + rootElement, + ); + + parallelTaskShapes.push(parallelTaskShape); + }); + + const parallelGatewayIncoming = modeling.createShape( + elementFactory.createShape({ + type: 'bpmn:ParallelGateway', + }), + { x: (newCurrentXPosition += xPositionOffset), y: yPosition }, + rootElement, + ); + + parallelTaskShapes.forEach((taskShape) => { + modeling.createConnection( + taskShape as any, + parallelGatewayIncoming, + { + type: 'bpmn:SequenceFlow', + }, + rootElement, + ); + }); + + newShapes.push(parallelGatewayOutgoing, ...parallelTaskShapes, parallelGatewayIncoming); + } else { + const task = currentPositionIDTasks[0]; + const taskShape = createTaskShape(bpmnModeler, task, { + x: (newCurrentXPosition += xPositionOffset), + y: yPosition, + }); + + newShapes.push(taskShape); + } + + // Connect shape of previous iteration to first shape created in this iteration (either task or outgoing parallel gateway) + modeling.createConnection( + createdShapes[createdShapes.length - 1] as any, + newShapes[0] as any, + { + type: 'bpmn:SequenceFlow', + }, + rootElement, + ); + + if (sortedTaskIdx === sortedTasks.length - 1) { + // Create end event and connect to last shape created in this iteration + const endEvent = modeling.createShape( + elementFactory.createShape({ + type: 'bpmn:EndEvent', + }), + { x: (newCurrentXPosition += xPositionOffset), y: yPosition }, + rootElement, + ); + modeling.createConnection( + newShapes[newShapes.length - 1] as any, + endEvent, + { + type: 'bpmn:SequenceFlow', + }, + rootElement, + ); + + newShapes.push(endEvent); + } + + return { + createdShapes: [...createdShapes, ...newShapes], + currentXPosition: newCurrentXPosition, + }; + }, + { createdShapes: [startEvent], currentXPosition: startEvent.x }, + ); + + const { xml } = await bpmnModeler.saveXML({ format: true }); + + if (!xml) { + throw new Error('Could not retrieve XML from modeler'); + } + + return xml; + }; + return ( <> { const processesData = await Promise.all( fileList.map(async (file) => { - // get the bpmn from the file and then extract relevant process meta data from the bpmn - const bpmn = await file.text(); + const fileText = await file.text(); + const bpmn = + env.PROCEED_PUBLIC_PROJECTS_HTA2 && file.type === 'application/json' + ? await importJsonProcess(fileText) + : fileText; const bpmnObj = await toBpmnObject(bpmn); diff --git a/src/management-system-v2/lib/env-vars.ts b/src/management-system-v2/lib/env-vars.ts index f4f96bed3..2fbf42cf8 100644 --- a/src/management-system-v2/lib/env-vars.ts +++ b/src/management-system-v2/lib/env-vars.ts @@ -22,6 +22,7 @@ const environmentVariables = { NODE_ENV: z.enum(['development', 'production', 'test']).default('development'), PROCEED_PUBLIC_ENABLE_EXECUTION: z.string().optional().transform(boolParser), PROCEED_PUBLIC_DEPLOYMENT_ENV: z.enum(['cloud', 'local']).optional(), + PROCEED_PUBLIC_PROJECTS_HTA2: z.string().optional(), NEXTAUTH_URL: z.string().default('http://localhost:3000'), SHARING_ENCRYPTION_SECRET: z.string(), INVITATION_ENCRYPTION_SECRET: z.string(),