diff --git a/README.md b/README.md index 1d97a20f..543db7c4 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Beside the dependencies provided in the Helm Chart, the following dependencies h | Application | App Version | Chart Version | |-------------------------------------------------------------------------------------------------------------------|-------------|---------------| -| [Tractus-X Connector](https://github.com/eclipse-tractusx/tractusx-edc/tree/main/charts/tractusx-connector) | 0.7.2 | 0.7.2 | +| [Tractus-X Connector](https://github.com/eclipse-tractusx/tractusx-edc/tree/main/charts/tractusx-connector) | 0.7.3 | 0.7.3 | | [Digital Twin Registry](https://github.com/eclipse-tractusx/sldt-digital-twin-registry/tree/main/charts/registry) | 0.5.0 | 0.5.0 | ## Known Knows diff --git a/backend/src/main/java/org/eclipse/tractusx/puris/backend/common/edc/logic/service/EdcAdapterService.java b/backend/src/main/java/org/eclipse/tractusx/puris/backend/common/edc/logic/service/EdcAdapterService.java index 75d48c4a..139a43d1 100644 --- a/backend/src/main/java/org/eclipse/tractusx/puris/backend/common/edc/logic/service/EdcAdapterService.java +++ b/backend/src/main/java/org/eclipse/tractusx/puris/backend/common/edc/logic/service/EdcAdapterService.java @@ -200,7 +200,7 @@ public boolean createPolicyAndContractDefForPartner(Partner partner) { private boolean createSubmodelContractDefinitionForPartner(String semanticId, String assetId, Partner partner) { var body = edcRequestBodyBuilder.buildSubmodelContractDefinitionWithBpnRestrictedPolicy(assetId, partner); - try (var response = sendPostRequest(body, List.of("v2", "contractdefinitions"))) { + try (var response = sendPostRequest(body, List.of("v3", "contractdefinitions"))) { if (!response.isSuccessful()) { log.warn("Contract definition registration failed for partner " + partner.getBpnl() + " and {} Submodel", semanticId); if (response.body() != null) { @@ -217,7 +217,7 @@ private boolean createSubmodelContractDefinitionForPartner(String semanticId, St private boolean createDtrContractDefinitionForPartner(Partner partner) { var body = edcRequestBodyBuilder.buildDtrContractDefinitionForPartner(partner); - try (var response = sendPostRequest(body, List.of("v2", "contractdefinitions"))) { + try (var response = sendPostRequest(body, List.of("v3", "contractdefinitions"))) { if (!response.isSuccessful()) { log.warn("Contract definition registration failed for partner " + partner.getBpnl() + " and DTR"); return false; @@ -239,7 +239,7 @@ private boolean createDtrContractDefinitionForPartner(Partner partner) { */ private boolean createBpnlAndMembershipPolicyDefinitionForPartner(Partner partner) { var body = edcRequestBodyBuilder.buildBpnAndMembershipRestrictedPolicy(partner); - try (var response = sendPostRequest(body, List.of("v2", "policydefinitions"))) { + try (var response = sendPostRequest(body, List.of("v3", "policydefinitions"))) { if (!response.isSuccessful()) { log.warn("Policy Registration failed"); if (response.body() != null) { @@ -261,7 +261,7 @@ private boolean createBpnlAndMembershipPolicyDefinitionForPartner(Partner partne */ private boolean createContractPolicy() { var body = edcRequestBodyBuilder.buildFrameworkPolicy(); - try (var response = sendPostRequest(body, List.of("v2", "policydefinitions"))) { + try (var response = sendPostRequest(body, List.of("v3", "policydefinitions"))) { if (!response.isSuccessful()) { log.warn("Framework Policy Registration failed"); if (response.body() != null) { @@ -323,7 +323,7 @@ private boolean sendAssetRegistrationRequest(JsonNode body, String assetId) { * @return The response containing the full catalog, if successful */ public Response getCatalogResponse(String dspUrl, String partnerBpnl, Map filter) throws IOException { - return sendPostRequest(edcRequestBodyBuilder.buildBasicCatalogRequestBody(dspUrl, partnerBpnl, filter), List.of("v2", "catalog", "request")); + return sendPostRequest(edcRequestBodyBuilder.buildBasicCatalogRequestBody(dspUrl, partnerBpnl, filter), List.of("v3", "catalog", "request")); } /** @@ -374,7 +374,7 @@ private JsonNode initiateNegotiation(Partner partner, JsonNode catalogItem, Stri // use dspUrl as provided, if set - else use partner dspUrl = dspUrl != null && !dspUrl.isEmpty() ? dspUrl : partner.getEdcUrl(); var requestBody = edcRequestBodyBuilder.buildAssetNegotiationBody(partner, catalogItem, dspUrl); - try (Response response = sendPostRequest(requestBody, List.of("v2", "contractnegotiations"))) { + try (Response response = sendPostRequest(requestBody, List.of("v3", "contractnegotiations"))) { JsonNode responseNode = objectMapper.readTree(response.body().string()); log.debug("Result from negotiation {}", responseNode.toPrettyString()); return responseNode; @@ -391,7 +391,7 @@ private JsonNode initiateNegotiation(Partner partner, JsonNode catalogItem, Stri * @throws IOException If the connection to your control plane fails */ public JsonNode getNegotiationState(String negotiationId) throws IOException { - try (var response = sendGetRequest(List.of("v2", "contractnegotiations", negotiationId))) { + try (var response = sendGetRequest(List.of("v3", "contractnegotiations", negotiationId))) { String stringData = response.body().string(); return objectMapper.readTree(stringData); } @@ -406,7 +406,7 @@ public JsonNode getNegotiationState(String negotiationId) throws IOException { */ public Response getAllNegotiations() throws IOException { var requestBody = edcRequestBodyBuilder.buildNegotiationsRequestBody(); - return sendPostRequest(requestBody, List.of("v2", "contractnegotiations", "request")); + return sendPostRequest(requestBody, List.of("v3", "contractnegotiations", "request")); } /** @@ -421,7 +421,7 @@ public Response getAllNegotiations() throws IOException { */ public JsonNode initiateProxyPullTransfer(Partner partner, String contractId, String assetId, String partnerEdcUrl) throws IOException { var body = edcRequestBodyBuilder.buildProxyPullRequestBody(partner, contractId, assetId, partnerEdcUrl); - try (var response = sendPostRequest(body, List.of("v2", "transferprocesses"))) { + try (var response = sendPostRequest(body, List.of("v3", "transferprocesses"))) { String data = response.body().string(); JsonNode result = objectMapper.readTree(data); log.debug("Got response from Proxy pull transfer init: {}", result.toPrettyString()); @@ -443,7 +443,7 @@ public JsonNode initiateProxyPullTransfer(Partner partner, String contractId, St * @throws IOException If the connection to your control plane fails */ public JsonNode getTransferState(String transferId) throws IOException { - try (var response = sendGetRequest(List.of("v2", "transferprocesses", transferId))) { + try (var response = sendGetRequest(List.of("v3", "transferprocesses", transferId))) { String data = response.body().string(); return objectMapper.readTree(data); } @@ -459,7 +459,7 @@ public JsonNode getTransferState(String transferId) throws IOException { public Response getAllTransfers() throws IOException { var requestBody = edcRequestBodyBuilder.buildTransfersRequestBody(); log.debug("GetAllTransfers Request: {}", requestBody.toPrettyString()); - return sendPostRequest(requestBody, List.of("v2", "transferprocesses", "request")); + return sendPostRequest(requestBody, List.of("v3", "transferprocesses", "request")); } /** @@ -471,7 +471,7 @@ public Response getAllTransfers() throws IOException { * @throws IOException If the connection to your control plane fails */ public String getContractAgreement(String contractAgreementId) throws IOException { - try (var response = sendGetRequest(List.of("v2", "contractagreements", contractAgreementId))) { + try (var response = sendGetRequest(List.of("v3", "contractagreements", contractAgreementId))) { return response.body().string(); } } @@ -985,7 +985,7 @@ private void terminateTransfer(String transferProcessId) { JsonNode body = edcRequestBodyBuilder.buildTransferProcessTerminationBody("Transfer done."); - try (Response response = sendPostRequest(body, List.of("v2", "transferprocesses", transferProcessId, "terminate"))) { + try (Response response = sendPostRequest(body, List.of("v3", "transferprocesses", transferProcessId, "terminate"))) { JsonNode resultNode = objectMapper.readTree(response.body().string()); if (!response.isSuccessful()) { diff --git a/backend/src/main/java/org/eclipse/tractusx/puris/backend/demandandcapacitynotification/controller/DemandAndCapacityNotificationRequestApiController.java b/backend/src/main/java/org/eclipse/tractusx/puris/backend/demandandcapacitynotification/controller/DemandAndCapacityNotificationRequestApiController.java index 48655e66..5d6a7732 100644 --- a/backend/src/main/java/org/eclipse/tractusx/puris/backend/demandandcapacitynotification/controller/DemandAndCapacityNotificationRequestApiController.java +++ b/backend/src/main/java/org/eclipse/tractusx/puris/backend/demandandcapacitynotification/controller/DemandAndCapacityNotificationRequestApiController.java @@ -19,7 +19,10 @@ */ package org.eclipse.tractusx.puris.backend.demandandcapacitynotification.controller; +import com.fasterxml.jackson.databind.JsonNode; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import lombok.extern.slf4j.Slf4j; @@ -50,24 +53,26 @@ public class DemandAndCapacityNotificationRequestApiController { @Operation(summary = "This endpoint receives the DemandAndCapacityNotification 2.0.0 requests") @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Ok"), - @ApiResponse(responseCode = "400", description = "Bad Request"), - @ApiResponse(responseCode = "500", description = "Internal Server Error") + @ApiResponse(responseCode = "200", description = "Ok", content = @Content), + @ApiResponse(responseCode = "400", description = "Bad Request", content = @Content), + @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content) }) @PostMapping("request") - public ResponseEntity postDemandAndCapacityNotification( + public ResponseEntity postDemandAndCapacityNotification( @RequestHeader("edc-bpn") String bpnl, - @RequestBody String body) + @io.swagger.v3.oas.annotations.parameters.RequestBody(content = {@Content(examples = { + @ExampleObject(sample) + })}) + @RequestBody JsonNode body) { if (!bpnlPattern.matcher(bpnl).matches()) { log.warn("Rejecting request at DemandAndCapacityNotification request 2.0.0 endpoint. Invalid BPNL"); return ResponseEntity.badRequest().build(); } try { - var data = objectMapper.readTree(body); log.info("Received POST request for DemandAndCapacityNotification 2.0.0 with BPNL: " + bpnl); var notification = objectMapper.readValue( - data.get("content").get("demandAndCapacityNotification").toString(), + body.get("content").get("demandAndCapacityNotification").toString(), DemandAndCapacityNotificationSamm.class); demandAndCapacityNotificationRequestApiService.handleIncomingNotification(bpnl, notification); } catch (Exception e) { @@ -76,4 +81,44 @@ public ResponseEntity postDemandAndCapacityNo } return ResponseEntity.ok(null); } + + final static String sample = "{\n" + + " \"header\": {\n" + + " \"senderBpn\": \"BPNL7588787849VQ\",\n" + + " \"context\": \"CX-DemandAndCapacityNotification:2.0\",\n" + + " \"messageId\": \"3b4edc05-e214-47a1-b0c2-1d831cdd9ba9\",\n" + + " \"receiverBpn\": \"BPNL6666787765VQ\",\n" + + " \"sentDateTime\": \"2023-06-19T21:24:00+07:00\",\n" + + " \"version\": \"3.0.0\"\n" + + " },\n" + + " \"content\": {\n" + + " \"demandAndCapacityNotification\": {\n" + + " \"affectedSitesSender\": [\n" + + " \"BPNS7588787849VQ\"\n" + + " ],\n" + + " \"affectedSitesRecipient\": [\n" + + " \"BPNS6666787765VQ\"\n" + + " ],\n" + + " \"materialNumberSupplier\": [\n" + + " \"MNR-8101-ID146955.001\"\n" + + " ],\n" + + " \"contentChangedAt\": \"2023-12-13T15:00:00+01:00\",\n" + + " \"startDateOfEffect\": \"2023-12-13T15:00:00+01:00\",\n" + + " \"relatedNotificationId\": \"urn:uuid:d05cef4a-b692-45bf-87cc-eda2d84e4c04\",\n" + + " \"materialNumberCustomer\": [\n" + + " \"MNR-7307-AU340474.002\"\n" + + " ],\n" + + " \"leadingRootCause\": \"strike\",\n" + + " \"materialGlobalAssetId\": [\n" + + " \"urn:uuid:48878d48-6f1d-47f5-8ded-a441d0d879df\"\n" + + " ],\n" + + " \"effect\": \"demand-reduction\",\n" + + " \"notificationId\": \"urn:uuid:d9452f24-3bf3-4134-b3eb-68858f1b2362\",\n" + + " \"text\": \"Capacity reduction due to ongoing strike.\",\n" + + " \"expectedEndDateOfEffect\": \"2023-12-17T08:00:00+01:00\",\n" + + " \"sourceNotificationId\": \"urn:uuid:c69cb3e4-16ad-43c3-82b9-0deac75ecf9e\",\n" + + " \"status\": \"resolved\"\n" + + " }\n" + + " }\n" + + "}"; } diff --git a/charts/puris/README.md b/charts/puris/README.md index 781bac80..db408c0f 100644 --- a/charts/puris/README.md +++ b/charts/puris/README.md @@ -148,7 +148,7 @@ dependencies: | frontend.autoscaling.minReplicas | int | `1` | Number of minimum replica pods for autoscaling | | frontend.autoscaling.targetCPUUtilizationPercentage | int | `80` | Value of CPU usage in percentage for autoscaling decisions | | frontend.env | object | `{}` | Extra environment variables that will be passed onto the frontend deployment pods | -| frontend.image.pullPolicy | string | `"IfNotPresent"` | THe policy for the image pull process | +| frontend.image.pullPolicy | string | `"Always"` | THe policy for the image pull process | | frontend.image.repository | string | `"tractusx/app-puris-frontend"` | Repository of the docker image | | frontend.image.tag | string | `""` | Overrides the image tag whose default is the chart appVersion. | | frontend.imagePullSecrets | list | `[]` | List of used secrets | @@ -172,6 +172,7 @@ dependencies: | frontend.puris.endpointCustomer | string | `"stockView/customer?ownMaterialNumber="` | The endpoint for the customers who buy a material identified via the own material number for the stock view | | frontend.puris.endpointDelivery | string | `"delivery"` | The endpoint for the delivery submodel | | frontend.puris.endpointDemand | string | `"demand"` | The endpoint for the demand submodel | +| frontend.puris.endpointErpScheduleUpdate | string | `"erp-adapter/trigger"` | The endpoint for scheduling an update of erp data (currently only stock supported) | | frontend.puris.endpointMaterialStocks | string | `"stockView/material-stocks"` | The endpoint for material stocks for the stock view | | frontend.puris.endpointMaterials | string | `"stockView/materials"` | The endpoint for materials for the stock view | | frontend.puris.endpointPartners | string | `"partners"` | The endpoint for partner information | diff --git a/charts/puris/templates/frontend-deployment.yaml b/charts/puris/templates/frontend-deployment.yaml index 46e87bd6..354faaf8 100644 --- a/charts/puris/templates/frontend-deployment.yaml +++ b/charts/puris/templates/frontend-deployment.yaml @@ -80,6 +80,8 @@ spec: value: "{{ .Values.frontend.puris.endpointUpdateReportedMaterialStocks }}" - name: ENDPOINT_UPDATE_REPORTED_PRODUCT_STOCKS value: "{{ .Values.frontend.puris.endpointUpdateReportedProductStocks }}" + - name: ENDPOINT_ERP_SCHEDULE_UPDATE + value: "{{ .Values.frontend.puris.endpointErpScheduleUpdate }}" - name: ENDPOINT_PARTNER value: "{{ .Values.frontend.puris.endpointPartners }}" - name: ENDPOINT_DEMAND diff --git a/charts/puris/values.yaml b/charts/puris/values.yaml index 455d30fc..86558799 100644 --- a/charts/puris/values.yaml +++ b/charts/puris/values.yaml @@ -32,7 +32,7 @@ frontend: # -- Repository of the docker image repository: tractusx/app-puris-frontend # -- THe policy for the image pull process - pullPolicy: IfNotPresent + pullPolicy: Always # -- Overrides the image tag whose default is the chart appVersion. tag: "" @@ -183,6 +183,8 @@ frontend: endpointUpdateReportedMaterialStocks: stockView/update-reported-material-stocks?ownMaterialNumber= # -- The endpoint for triggering an update of your product stocks on your partners side endpointUpdateReportedProductStocks: stockView/update-reported-product-stocks?ownMaterialNumber= + # -- The endpoint for scheduling an update of erp data (currently only stock supported) + endpointErpScheduleUpdate: erp-adapter/trigger # -- The endpoint for partner information endpointPartners: partners # -- The endpoint for the demand submodel diff --git a/frontend/.env b/frontend/.env index 2f31b2e1..67789322 100644 --- a/frontend/.env +++ b/frontend/.env @@ -13,6 +13,7 @@ VITE_ENDPOINT_REPORTED_MATERIAL_STOCKS=stockView/reported-material-stocks?ownMat VITE_ENDPOINT_REPORTED_PRODUCT_STOCKS=stockView/reported-product-stocks?ownMaterialNumber= VITE_ENDPOINT_UPDATE_REPORTED_MATERIAL_STOCKS=stockView/update-reported-material-stocks?ownMaterialNumber= VITE_ENDPOINT_UPDATE_REPORTED_PRODUCT_STOCKS=stockView/update-reported-product-stocks?ownMaterialNumber= +VITE_ENDPOINT_ERP_SCHEDULE_UPDATE=erp-adapter/trigger VITE_ENDPOINT_PARTNER=partners VITE_ENDPOINT_DEMAND=demand VITE_ENDPOINT_PRODUCTION=production diff --git a/frontend/.env.dockerbuild b/frontend/.env.dockerbuild index ec2f7582..af4e7725 100644 --- a/frontend/.env.dockerbuild +++ b/frontend/.env.dockerbuild @@ -11,6 +11,7 @@ VITE_ENDPOINT_REPORTED_MATERIAL_STOCKS=\$ENDPOINT_REPORTED_MATERIAL_STOCKS VITE_ENDPOINT_REPORTED_PRODUCT_STOCKS=\$ENDPOINT_REPORTED_PRODUCT_STOCKS VITE_ENDPOINT_UPDATE_REPORTED_MATERIAL_STOCKS=\$ENDPOINT_UPDATE_REPORTED_MATERIAL_STOCKS VITE_ENDPOINT_UPDATE_REPORTED_PRODUCT_STOCKS=\$ENDPOINT_UPDATE_REPORTED_PRODUCT_STOCKS +VITE_ENDPOINT_ERP_SCHEDULE_UPDATE=\$ENDPOINT_ERP_SCHEDULE_UPDATE VITE_ENDPOINT_PARTNER=\$ENDPOINT_PARTNER VITE_ENDPOINT_DEMAND=\$ENDPOINT_DEMAND VITE_ENDPOINT_PRODUCTION=\$ENDPOINT_PRODUCTION diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 10526d29..04c5905c 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -18,7 +18,7 @@ # # SPDX-License-Identifier: Apache-2.0 # -FROM node:lts-alpine as build +FROM node:lts-alpine AS build ARG NPM_BUILD_MODE=dockerbuild diff --git a/frontend/src/config.json b/frontend/src/config.json index 7b8d422a..09e76a63 100644 --- a/frontend/src/config.json +++ b/frontend/src/config.json @@ -12,6 +12,7 @@ "ENDPOINT_REPORTED_PRODUCT_STOCKS": "$ENDPOINT_REPORTED_PRODUCT_STOCKS", "ENDPOINT_UPDATE_REPORTED_MATERIAL_STOCKS":"$ENDPOINT_UPDATE_REPORTED_MATERIAL_STOCKS", "ENDPOINT_UPDATE_REPORTED_PRODUCT_STOCKS":"$ENDPOINT_UPDATE_REPORTED_MATERIAL_STOCKS", + "ENDPOINT_ERP_SCHEDULE_UPDATE":"$ENDPOINT_ERP_SCHEDULE_UPDATE", "ENDPOINT_PARTNER": "$ENDPOINT_PARTNER", "ENDPOINT_DEMAND": "$ENDPOINT_DEMAND", "ENDPOINT_PRODUCTION": "$ENDPOINT_PRODUCTION", diff --git a/frontend/src/features/dashboard/components/Dashboard.tsx b/frontend/src/features/dashboard/components/Dashboard.tsx index 36992a37..fa7a4ee9 100644 --- a/frontend/src/features/dashboard/components/Dashboard.tsx +++ b/frontend/src/features/dashboard/components/Dashboard.tsx @@ -1,5 +1,6 @@ /* Copyright (c) 2024 Volkswagen AG +Copyright (c) 2024 Fraunhofer-Gesellschaft zur Foerderung der angewandten Forschung e.V. (represented by Fraunhofer ISST) Copyright (c) 2024 Contributors to the Eclipse Foundation See the NOTICE file(s) distributed with this work for additional @@ -21,15 +22,15 @@ import { usePartnerStocks } from '@features/stock-view/hooks/usePartnerStocks'; import { useStocks } from '@features/stock-view/hooks/useStocks'; import { MaterialDescriptor } from '@models/types/data/material-descriptor'; import { Site } from '@models/types/edc/site'; -import { useCallback, useReducer } from 'react'; +import { useCallback, useReducer, useState } from 'react'; import { DashboardFilters } from './DashboardFilters'; import { DemandTable } from './DemandTable'; import { ProductionTable } from './ProductionTable'; -import { Box, Button, Stack, Typography, capitalize } from '@mui/material'; +import { Box, Button, capitalize, Stack, Typography } from '@mui/material'; import { Delivery } from '@models/types/data/delivery'; import { DeliveryInformationModal } from './DeliveryInformationModal'; import { getPartnerType } from '../util/helpers'; -import { LoadingButton } from '@catena-x/portal-shared-components'; +import { LoadingButton, PageSnackbar, PageSnackbarStack } from '@catena-x/portal-shared-components'; import { Refresh } from '@mui/icons-material'; import { Demand } from '@models/types/data/demand'; import { DemandCategoryModal } from './DemandCategoryModal'; @@ -41,12 +42,13 @@ import { PlannedProductionModal } from './PlannedProductionModal'; import { useProduction } from '../hooks/useProduction'; import { useReportedProduction } from '../hooks/useReportedProduction'; -import { requestReportedStocks } from '@services/stocks-service'; +import { requestReportedStocks, scheduleErpUpdateStocks } from '@services/stocks-service'; import { useDelivery } from '../hooks/useDelivery'; import { requestReportedDeliveries } from '@services/delivery-service'; import { requestReportedProductions } from '@services/productions-service'; import { requestReportedDemands } from '@services/demands-service'; import { ModalMode } from '@models/types/data/modal-mode'; +import { Notification } from "@models/types/data/notification.ts"; const NUMBER_OF_DAYS = 28; @@ -61,6 +63,7 @@ type DashboardState = { demand: Partial | null; production: Partial | null; isRefreshing: boolean; + isErpRefreshing: false; }; type DashboardAction = { @@ -83,6 +86,7 @@ const initialState: DashboardState = { demand: null, production: null, isRefreshing: false, + isErpRefreshing: false, }; export const Dashboard = ({ type }: { type: 'customer' | 'supplier' }) => { @@ -104,15 +108,68 @@ export const Dashboard = ({ type }: { type: 'customer' | 'supplier' }) => { state.selectedSite?.bpns ?? null ); + const [notifications, setNotifications] = useState([]); + const handleRefresh = () => { - dispatch({ type: 'isRefreshing', payload: true }); + dispatch({type: 'isRefreshing', payload: true}); Promise.all([ requestReportedStocks(type === 'customer' ? 'material' : 'product', state.selectedMaterial?.ownMaterialNumber ?? null), requestReportedDeliveries(state.selectedMaterial?.ownMaterialNumber ?? null), type === 'customer' ? requestReportedProductions(state.selectedMaterial?.ownMaterialNumber ?? null) : requestReportedDemands(state.selectedMaterial?.ownMaterialNumber ?? null) - ]).finally(() => dispatch({ type: 'isRefreshing', payload: false })); + ]).then(() => { + setNotifications(ns => [ + ...ns, + { + title: 'Update requested', + description: `Requested update from partners for ${state.selectedMaterial?.ownMaterialNumber}. Please reload dialog later.`, + severity: 'success', + }, + ]); + }).catch((error: unknown) => { + const msg = error !== null && typeof error === 'object' && 'message' in error && typeof error.message === 'string' ? error.message : 'Unknown Error'; + setNotifications(ns => [ + ...ns, + { + title: 'Error requesting update', + description: msg, + severity: 'error', + }, + ]); + }).finally(() => dispatch({type: 'isRefreshing', payload: false})) + }; + // }; + const handleScheduleErpUpdate = () => { + dispatch({ type: 'isErpRefreshing', payload: true }); + if (state.selectedPartnerSites){ + const promises: Promise[] = state.selectedPartnerSites.map((ps: Site) => { + return scheduleErpUpdateStocks(type === 'customer' ? 'material' : 'product', ps.belongsToPartnerBpnl, state.selectedMaterial?.ownMaterialNumber ?? null); + }); + Promise.all(promises) + .then(() => { + setNotifications(ns => [ + ...ns, + { + title: 'Update requested', + description: `Scheduled ERP data update of stocks for ${state.selectedMaterial?.ownMaterialNumber} in your role as ${type}. Please reload dialog later.`, + severity: 'success', + }, + ]); + }) + .catch((error: unknown) => { + const msg = error !== null && typeof error === 'object' && 'message' in error && typeof error.message === 'string' ? error.message : 'Unknown Error'; + setNotifications(ns => [ + ...ns, + { + title: 'Error scheduling ERP update', + description: msg, + severity: 'error', + }, + ]); + }) + .finally(() => dispatch({type: 'isErpRefreshing', payload:false })); + } }; const openDeliveryDialog = useCallback( (d: Partial, mode: ModalMode, direction: 'incoming' | 'outgoing' = 'outgoing', site: Site | null) => { @@ -193,12 +250,40 @@ export const Dashboard = ({ type }: { type: 'customer' | 'supplier' }) => { {state.selectedSite && ( - + {`${capitalize(getPartnerType(type))} Information ${ state.selectedMaterial ? `for ${state.selectedMaterial.description} (${state.selectedMaterial.ownMaterialNumber})` : '' }`} + + {state.selectedPartnerSites?.length && + (state.isErpRefreshing ? ( + + ) : ( + + ))} {state.selectedPartnerSites?.length && (state.isRefreshing ? ( { Refresh ))} + {state.selectedPartnerSites ? ( @@ -280,6 +366,19 @@ export const Dashboard = ({ type }: { type: 'customer' | 'supplier' }) => { delivery={state.delivery} deliveries={deliveries ?? []} /> + + {notifications.map((notification, index) => ( + setNotifications((ns) => ns.filter((_, i) => i !== index) ?? [])} + /> + ))} + ); } diff --git a/frontend/src/features/dashboard/components/DashboardFilters.tsx b/frontend/src/features/dashboard/components/DashboardFilters.tsx index da7ed10f..5159bd31 100644 --- a/frontend/src/features/dashboard/components/DashboardFilters.tsx +++ b/frontend/src/features/dashboard/components/DashboardFilters.tsx @@ -1,5 +1,6 @@ /* Copyright (c) 2024 Volkswagen AG +Copyright (c) 2024 Fraunhofer-Gesellschaft zur Foerderung der angewandten Forschung e.V. (represented by Fraunhofer ISST) Copyright (c) 2024 Contributors to the Eclipse Foundation See the NOTICE file(s) distributed with this work for additional @@ -24,7 +25,7 @@ import { usePartners } from '@features/stock-view/hooks/usePartners'; import { useSites } from '@features/stock-view/hooks/useSites'; import { MaterialDescriptor } from '@models/types/data/material-descriptor'; import { Site } from '@models/types/edc/site'; -import { Autocomplete, Grid, InputLabel, capitalize } from '@mui/material'; +import { Autocomplete, capitalize, Grid, InputLabel } from '@mui/material'; import { getPartnerType } from '../util/helpers'; import { LabelledAutoComplete } from '@components/ui/LabelledAutoComplete'; @@ -80,7 +81,13 @@ export const DashboardFilters = ({ [...acc, ...p.sites], []) ?? []} + options={partners?.reduce((acc: Site[], p) => { + const sitesWithBpnl = p.sites.map(site => ({ + ...site, + belongsToPartnerBpnl: p.bpnl + })); + return [...acc, ...sitesWithBpnl]; + }, []) ?? [] } disabled={!site} getOptionLabel={(option) => `${option.name} (${option.bpns})`} isOptionEqualToValue={(option, value) => option.bpns === value.bpns} diff --git a/frontend/src/models/constants/config.ts b/frontend/src/models/constants/config.ts index baf3412e..cb66caa4 100644 --- a/frontend/src/models/constants/config.ts +++ b/frontend/src/models/constants/config.ts @@ -33,6 +33,7 @@ const app = { ENDPOINT_REPORTED_PRODUCT_STOCKS: import.meta.env.VITE_ENDPOINT_REPORTED_PRODUCT_STOCKS.trim() as string, ENDPOINT_UPDATE_REPORTED_MATERIAL_STOCKS: import.meta.env.VITE_ENDPOINT_UPDATE_REPORTED_MATERIAL_STOCKS.trim() as string, ENDPOINT_UPDATE_REPORTED_PRODUCT_STOCKS: import.meta.env.VITE_ENDPOINT_UPDATE_REPORTED_PRODUCT_STOCKS.trim() as string, + ENDPOINT_ERP_SCHEDULE_UPDATE: import.meta.env.VITE_ENDPOINT_ERP_SCHEDULE_UPDATE.trim() as string, ENDPOINT_PARTNER: import.meta.env.VITE_ENDPOINT_PARTNER.trim() as string, ENDPOINT_DEMAND: import.meta.env.VITE_ENDPOINT_DEMAND.trim() as string, ENDPOINT_PRODUCTION: import.meta.env.VITE_ENDPOINT_PRODUCTION.trim() as string, diff --git a/frontend/src/models/types/edc/site.ts b/frontend/src/models/types/edc/site.ts index 933efdb0..b78e270f 100644 --- a/frontend/src/models/types/edc/site.ts +++ b/frontend/src/models/types/edc/site.ts @@ -1,5 +1,6 @@ /* Copyright (c) 2024 Volkswagen AG +Copyright (c) 2024 Fraunhofer-Gesellschaft zur Foerderung der angewandten Forschung e.V. (represented by Fraunhofer ISST) Copyright (c) 2024 Contributors to the Eclipse Foundation See the NOTICE file(s) distributed with this work for additional @@ -19,10 +20,11 @@ SPDX-License-Identifier: Apache-2.0 */ import { Address } from './address'; -import { BPNS } from './bpn'; +import { BPNL, BPNS } from './bpn'; export type Site = { bpns: BPNS; name: string; addresses: Address[]; + belongsToPartnerBpnl: BPNL; }; diff --git a/frontend/src/models/types/erp/assetType.ts b/frontend/src/models/types/erp/assetType.ts new file mode 100644 index 00000000..3e346095 --- /dev/null +++ b/frontend/src/models/types/erp/assetType.ts @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024 Fraunhofer-Gesellschaft zur Foerderung der angewandten Forschung e.V. (represented by Fraunhofer ISST) + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export enum AssetType { + ItemStock = 'ITEM_STOCK_SUBMODEL', + Production = 'PRODUCTION_SUBMODEL', + Delivery = 'DELIVERY_INFORMATION_SUBMODEL', + Demand = 'DEMAND_SUBMODEL', + PartType = 'PART_TYPE_INFORMATION_SUBMODEL', +} diff --git a/frontend/src/models/types/erp/directionType.ts b/frontend/src/models/types/erp/directionType.ts new file mode 100644 index 00000000..9a1a3171 --- /dev/null +++ b/frontend/src/models/types/erp/directionType.ts @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2024 Fraunhofer-Gesellschaft zur Foerderung der angewandten Forschung e.V. (represented by Fraunhofer ISST) + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export enum DirectionType { + Inbound = 'INBOUND', + Outbound = 'OUTBOUND', +} diff --git a/frontend/src/services/erp-service.ts b/frontend/src/services/erp-service.ts new file mode 100644 index 00000000..246d587a --- /dev/null +++ b/frontend/src/services/erp-service.ts @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2024 Fraunhofer-Gesellschaft zur Foerderung der angewandten Forschung e.V. (represented by Fraunhofer ISST) + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { config } from '@models/constants/config'; +import { AssetType } from '@models/types/erp/assetType'; +import { DirectionType } from '@models/types/erp/directionType'; + +const PARAM_BPNL = 'partner-bpnl'; +const PARAM_MATERIAL_NUMBER = 'own-materialnumber'; +const PARAM_ASSET_TYPE = 'asset-type'; +const PARAM_DIRECTION = 'direction'; + +export const scheduleErpUpdate = async (partnerBpnl: string | null, materialNumber: string | null, type: AssetType, direction: DirectionType): Promise => { + // assetType always ItemStock + if (type != AssetType.ItemStock) { + throw new Error("The ERP Adapter currently only implements ItemStock, you tried " + AssetType.ItemStock); + } + + const endpoint = config.app.ENDPOINT_ERP_SCHEDULE_UPDATE; + const res = await fetch(`${config.app.BACKEND_BASE_URL}${endpoint}?${PARAM_BPNL}=${partnerBpnl}&${PARAM_MATERIAL_NUMBER}=${materialNumber}&${PARAM_ASSET_TYPE}=${type}&${PARAM_DIRECTION}=${direction}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-API-KEY': config.app.BACKEND_API_KEY, + }, + }); + if(res.status >= 400) { + throw await res.json(); + } + return; +} diff --git a/frontend/src/services/stocks-service.ts b/frontend/src/services/stocks-service.ts index 0703fcf2..f4c29332 100644 --- a/frontend/src/services/stocks-service.ts +++ b/frontend/src/services/stocks-service.ts @@ -1,5 +1,6 @@ /* Copyright (c) 2023,2024 Volkswagen AG +Copyright (c) 2024 Fraunhofer-Gesellschaft zur Foerderung der angewandten Forschung e.V. (represented by Fraunhofer ISST) Copyright (c) 2023,2024 Contributors to the Eclipse Foundation See the NOTICE file(s) distributed with this work for additional @@ -20,6 +21,9 @@ SPDX-License-Identifier: Apache-2.0 import { config } from '@models/constants/config'; import { Stock, StockType } from '@models/types/data/stock'; +import { scheduleErpUpdate } from '@services/erp-service' +import { AssetType } from "@models/types/erp/assetType.ts"; +import { DirectionType } from "@models/types/erp/directionType.ts"; export const postStocks = async (type: StockType, stock: Stock) => { const endpoint = type === 'product' ? config.app.ENDPOINT_PRODUCT_STOCKS : config.app.ENDPOINT_MATERIAL_STOCKS; @@ -32,8 +36,7 @@ export const postStocks = async (type: StockType, stock: Stock) => { }, }); if(res.status >= 400) { - const error = await res.json(); - throw error; + throw await res.json(); } return res.json(); } @@ -49,8 +52,7 @@ export const putStocks = async (type: StockType, stock: Stock) => { }, }); if(res.status >= 400) { - const error = await res.json(); - throw error; + throw await res.json(); } return res.json(); } @@ -65,8 +67,16 @@ export const requestReportedStocks = async (type: StockType, materialNumber: str }, }); if(res.status >= 400) { - const error = await res.json(); - throw error; + throw await res.json(); } return res.json(); } + +export const scheduleErpUpdateStocks = async (type: StockType, partnerBpnl: string | null, materialNumber: string | null): Promise => { + // assetType always ItemStock + const assetType = AssetType.ItemStock; + // infer product = OUTBOUND, material = INBOUND + const direction = type === 'product' ? DirectionType.Outbound : DirectionType.Inbound; + + return scheduleErpUpdate(partnerBpnl, materialNumber, assetType, direction); +} diff --git a/local/docker-compose.yaml b/local/docker-compose.yaml index 6e71001a..1e0b865f 100644 --- a/local/docker-compose.yaml +++ b/local/docker-compose.yaml @@ -40,6 +40,7 @@ services: - ENDPOINT_REPORTED_PRODUCT_STOCKS=stockView/reported-product-stocks?ownMaterialNumber= - ENDPOINT_UPDATE_REPORTED_MATERIAL_STOCKS=stockView/update-reported-material-stocks?ownMaterialNumber= - ENDPOINT_UPDATE_REPORTED_PRODUCT_STOCKS=stockView/update-reported-product-stocks?ownMaterialNumber= + - ENDPOINT_ERP_SCHEDULE_UPDATE=erp-adapter/trigger - ENDPOINT_PARTNER=partners - ENDPOINT_DEMAND=demand - ENDPOINT_PRODUCTION=production @@ -197,6 +198,7 @@ services: - ENDPOINT_REPORTED_PRODUCT_STOCKS=stockView/reported-product-stocks?ownMaterialNumber= - ENDPOINT_UPDATE_REPORTED_MATERIAL_STOCKS=stockView/update-reported-material-stocks?ownMaterialNumber= - ENDPOINT_UPDATE_REPORTED_PRODUCT_STOCKS=stockView/update-reported-product-stocks?ownMaterialNumber= + - ENDPOINT_ERP_SCHEDULE_UPDATE=erp-adapter/trigger - ENDPOINT_PARTNER=partners - ENDPOINT_DEMAND=demand - ENDPOINT_PRODUCTION=production diff --git a/local/tractus-x-edc/config/customer/data-plane.properties b/local/tractus-x-edc/config/customer/data-plane.properties index 12e44d77..2201bdc9 100644 --- a/local/tractus-x-edc/config/customer/data-plane.properties +++ b/local/tractus-x-edc/config/customer/data-plane.properties @@ -57,3 +57,5 @@ edc.iam.trusted-issuer.portal.id=did:web:mock-util-service/trusted-issuer tx.iam.credentialservice.url=http://mock-util-service:80 # don't use https during did resolving in catalog request edc.iam.did.web.use.https=false + +edc.dpf.selector.url=http://customer-control-plane:8183/api/controlplane/control/v1/dataplanes diff --git a/local/tractus-x-edc/config/supplier/data-plane.properties b/local/tractus-x-edc/config/supplier/data-plane.properties index a419f1b4..c9acc4bc 100644 --- a/local/tractus-x-edc/config/supplier/data-plane.properties +++ b/local/tractus-x-edc/config/supplier/data-plane.properties @@ -58,3 +58,5 @@ edc.iam.trusted-issuer.portal.id=did:web:mock-util-service/trusted-issuer tx.iam.credentialservice.url=http://mock-util-service:80 # don't use https during did resolving in catalog request edc.iam.did.web.use.https=false + +edc.dpf.selector.url=http://supplier-control-plane:9183/api/controlplane/control/v1/dataplanes diff --git a/local/tractus-x-edc/docker-compose.yaml b/local/tractus-x-edc/docker-compose.yaml index 9d4b1794..cbd9a5c2 100644 --- a/local/tractus-x-edc/docker-compose.yaml +++ b/local/tractus-x-edc/docker-compose.yaml @@ -21,13 +21,13 @@ version: "3" services: control-plane: - image: tractusx/edc-controlplane-postgresql-hashicorp-vault:0.7.2 + image: tractusx/edc-controlplane-postgresql-hashicorp-vault:0.7.3 volumes: - ./config/default/opentelemetry.properties:/app/opentelemetry.properties - ./config/default/logging.properties:/app/logging.properties data-plane: - image: tractusx/edc-dataplane-hashicorp-vault:0.7.2 + image: tractusx/edc-dataplane-hashicorp-vault:0.7.3 volumes: - ./config/default/opentelemetry.properties:/app/opentelemetry.properties - ./config/default/logging.properties:/app/logging.properties