From eb5f3315a47383d3951e78081a896c3697d5d8d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Schr=C3=B6der?= <131770181+ReneSchroederLJ@users.noreply.github.com> Date: Fri, 14 Jun 2024 15:43:20 +0200 Subject: [PATCH 1/4] feat: implemented DemandAndCapacityNotification frontend --- ...mandAndCapacityNotificationController.java | 10 +- .../controller/PartnerController.java | 21 + frontend/.env | 4 +- frontend/src/components/layout/SideBar.tsx | 6 + .../NotificationInformationModal.tsx | 381 ++++++++++++++++++ .../features/stock-view/hooks/useMaterials.ts | 2 +- frontend/src/hooks/useAllMaterials.ts | 34 ++ .../useAllPartners.ts} | 10 +- frontend/src/hooks/usePartnerMaterials.ts | 35 ++ frontend/src/index.css | 12 +- frontend/src/models/constants/config.ts | 2 + frontend/src/models/constants/effects.ts | 45 +++ .../models/constants/leading-root-causes.ts | 49 +++ frontend/src/models/constants/status.ts | 37 ++ .../data/demand-capacity-notification.ts | 39 ++ .../models/types/data/material-descriptor.ts | 1 + frontend/src/router.tsx | 5 + .../services/demand-capacity-notification.ts | 56 +++ frontend/src/views/CatalogView.tsx | 4 +- .../views/DemandCapacityNotificationView.tsx | 132 ++++++ 20 files changed, 873 insertions(+), 12 deletions(-) create mode 100644 frontend/src/features/notifications/components/NotificationInformationModal.tsx create mode 100644 frontend/src/hooks/useAllMaterials.ts rename frontend/src/{features/edc/hooks/usePartners.ts => hooks/useAllPartners.ts} (77%) create mode 100644 frontend/src/hooks/usePartnerMaterials.ts create mode 100644 frontend/src/models/constants/effects.ts create mode 100644 frontend/src/models/constants/leading-root-causes.ts create mode 100644 frontend/src/models/constants/status.ts create mode 100644 frontend/src/models/types/data/demand-capacity-notification.ts create mode 100644 frontend/src/services/demand-capacity-notification.ts create mode 100644 frontend/src/views/DemandCapacityNotificationView.tsx diff --git a/backend/src/main/java/org/eclipse/tractusx/puris/backend/demandandcapacitynotification/controller/DemandAndCapacityNotificationController.java b/backend/src/main/java/org/eclipse/tractusx/puris/backend/demandandcapacitynotification/controller/DemandAndCapacityNotificationController.java index 20cb99f6..953f692b 100644 --- a/backend/src/main/java/org/eclipse/tractusx/puris/backend/demandandcapacitynotification/controller/DemandAndCapacityNotificationController.java +++ b/backend/src/main/java/org/eclipse/tractusx/puris/backend/demandandcapacitynotification/controller/DemandAndCapacityNotificationController.java @@ -190,7 +190,6 @@ private DemandAndCapacityNotificationDto convertToDto(ReportedDemandAndCapacityN dto.setPartnerBpnl(entity.getPartner().getBpnl()); return dto; } - private OwnDemandAndCapacityNotification convertToEntity(DemandAndCapacityNotificationDto dto) { OwnDemandAndCapacityNotification entity = modelMapper.map(dto, OwnDemandAndCapacityNotification.class); @@ -202,6 +201,9 @@ private OwnDemandAndCapacityNotification convertToEntity(DemandAndCapacityNotifi } entity.setPartner(existingPartner); + if (dto.getAffectedMaterialNumbers() == null) { + dto.setAffectedMaterialNumbers(new ArrayList<>()); + } List materials = new ArrayList<>(); for (String ownMaterialNumber : dto.getAffectedMaterialNumbers()) { Material material = materialService.findByOwnMaterialNumber(ownMaterialNumber); @@ -214,6 +216,9 @@ private OwnDemandAndCapacityNotification convertToEntity(DemandAndCapacityNotifi } entity.setMaterials(materials); + if (dto.getAffectedSitesBpnsRecipient() == null) { + dto.setAffectedSitesBpnsRecipient(new ArrayList<>()); + } List affectedSitesRecipient = new ArrayList<>(); for (String bpns : dto.getAffectedSitesBpnsRecipient()) { Site site = existingPartner.getSites().stream().filter(p -> p.getBpns().equals(bpns)).findFirst() @@ -228,6 +233,9 @@ private OwnDemandAndCapacityNotification convertToEntity(DemandAndCapacityNotifi entity.setAffectedSitesRecipient(affectedSitesRecipient); Partner ownPartner = partnerService.getOwnPartnerEntity(); + if (dto.getAffectedSitesBpnsSender() == null) { + dto.setAffectedSitesBpnsSender(new ArrayList<>()); + } List affectedSitesSender = new ArrayList<>(); for (String bpns : dto.getAffectedSitesBpnsSender()) { Site site = ownPartner.getSites().stream().filter(p -> p.getBpns().equals(bpns)).findFirst().orElse(null); diff --git a/backend/src/main/java/org/eclipse/tractusx/puris/backend/masterdata/controller/PartnerController.java b/backend/src/main/java/org/eclipse/tractusx/puris/backend/masterdata/controller/PartnerController.java index 0cb52292..2c0bfdc3 100644 --- a/backend/src/main/java/org/eclipse/tractusx/puris/backend/masterdata/controller/PartnerController.java +++ b/backend/src/main/java/org/eclipse/tractusx/puris/backend/masterdata/controller/PartnerController.java @@ -29,11 +29,13 @@ import lombok.extern.slf4j.Slf4j; import org.eclipse.tractusx.puris.backend.common.util.PatternStore; import org.eclipse.tractusx.puris.backend.masterdata.domain.model.Address; +import org.eclipse.tractusx.puris.backend.masterdata.domain.model.Material; import org.eclipse.tractusx.puris.backend.masterdata.domain.model.Partner; import org.eclipse.tractusx.puris.backend.masterdata.domain.model.Site; import org.eclipse.tractusx.puris.backend.masterdata.logic.dto.AddressDto; import org.eclipse.tractusx.puris.backend.masterdata.logic.dto.PartnerDto; import org.eclipse.tractusx.puris.backend.masterdata.logic.dto.SiteDto; +import org.eclipse.tractusx.puris.backend.masterdata.logic.service.MaterialPartnerRelationService; import org.eclipse.tractusx.puris.backend.masterdata.logic.service.PartnerService; import org.modelmapper.Conditions; import org.modelmapper.ModelMapper; @@ -60,6 +62,9 @@ public class PartnerController { private Validator validator; private final ModelMapper modelMapper = new ModelMapper(); + @Autowired + private MaterialPartnerRelationService mpr; + private final Pattern bpnlPattern = PatternStore.BPNL_PATTERN; @PostMapping @@ -236,4 +241,20 @@ public ResponseEntity> getOwnSites() { HttpStatus.OK); } + @GetMapping("{partnerBpnl}/materials") + @Operation(description = "Returns all materials the specified partner is associated with.") + public ResponseEntity> getMaterials(@PathVariable String partnerBpnl) { + if (!bpnlPattern.matcher(partnerBpnl).matches()) { + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + } + + Partner partner = partnerService.findByBpnl(partnerBpnl); + if (partner == null) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + return new ResponseEntity<>(mpr.findAll().stream() + .filter(rel -> rel.getPartner().equals(partner)) + .map(rel -> rel.getMaterial()).collect(Collectors.toList()), HttpStatus.OK); + } + } diff --git a/frontend/.env b/frontend/.env index 82961c5f..2f31b2e1 100644 --- a/frontend/.env +++ b/frontend/.env @@ -2,7 +2,8 @@ VITE_APP_NAME=PURIS VITE_BACKEND_BASE_URL=http://localhost:8081/catena/ VITE_BACKEND_API_KEY=test -VITE_ENDPOINT_MATERIALS=stockView/materials +VITE_ENDPOINT_STOCK_VIEW_MATERIALS=stockView/materials +VITE_ENDPOINT_MATERIALS=materials VITE_ENDPOINT_PRODUCTS=stockView/products VITE_ENDPOINT_MATERIAL_STOCKS=stockView/material-stocks VITE_ENDPOINT_PRODUCT_STOCKS=stockView/product-stocks @@ -17,6 +18,7 @@ VITE_ENDPOINT_DEMAND=demand VITE_ENDPOINT_PRODUCTION=production VITE_ENDPOINT_PRODUCTION_RANGE=production/range VITE_ENDPOINT_DELIVERY=delivery +VITE_ENDPOINT_DEMAND_AND_CAPACITY_NOTIFICATION=demand-and-capacity-notification VITE_IDP_DISABLE=true VITE_IDP_URL=http://localhost:10081/ diff --git a/frontend/src/components/layout/SideBar.tsx b/frontend/src/components/layout/SideBar.tsx index 3df4830d..9206efb9 100644 --- a/frontend/src/components/layout/SideBar.tsx +++ b/frontend/src/components/layout/SideBar.tsx @@ -28,6 +28,7 @@ import { Typography } from '@catena-x/portal-shared-components'; import { Role } from '@models/types/auth/role'; import { useAuth } from '@hooks/useAuth'; import { Handshake, Logout, SyncAlt } from '@mui/icons-material'; +import NotificationsIcon from '@mui/icons-material/Notifications'; import { OverridableComponent } from '@mui/material/OverridableComponent'; import { SvgIconTypeMap } from '@mui/material'; import AuthenticationService from '@services/authentication-service'; @@ -76,6 +77,11 @@ const sideBarItems: SideBarItemProps[] = [ path: '/transfers', requiredRoles: ['PURIS_ADMIN'], }, + { + name: 'Notifications', + icon: NotificationsIcon, + path: '/notifications', + }, { name: 'Logout', icon: Logout, diff --git a/frontend/src/features/notifications/components/NotificationInformationModal.tsx b/frontend/src/features/notifications/components/NotificationInformationModal.tsx new file mode 100644 index 00000000..3187277d --- /dev/null +++ b/frontend/src/features/notifications/components/NotificationInformationModal.tsx @@ -0,0 +1,381 @@ +/* +Copyright (c) 2024 Volkswagen AG +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 { Input, PageSnackbar, PageSnackbarStack, Textarea } from '@catena-x/portal-shared-components'; +import { DateTime } from '@components/ui/DateTime'; +import { Close, Save } from '@mui/icons-material'; +import { Autocomplete, Box, Button, Dialog, DialogTitle, FormLabel, Grid, InputLabel, Stack } from '@mui/material'; +import { useEffect, useState } from 'react'; +import { LabelledAutoComplete } from '@components/ui/LabelledAutoComplete'; +import { postDemandAndCapacityNotification } from '@services/demand-capacity-notification'; +import { Notification } from '@models/types/data/notification'; +import { EFFECTS } from '@models/constants/effects'; +import { useAllPartners } from '@hooks/useAllPartners'; +import { LEADING_ROOT_CAUSE } from '@models/constants/leading-root-causes'; +import { STATUS } from '@models/constants/status'; +import { DemandCapacityNotification } from '@models/types/data/demand-capacity-notification'; +import { Site } from '@models/types/edc/site'; +import { useSites } from '@features/stock-view/hooks/useSites'; +import { usePartnerMaterials } from '@hooks/usePartnerMaterials'; +import { Partner } from '@models/types/edc/partner'; + +const isValidDemandCapacityNotification = (notification: Partial) => + notification.partnerBpnl && + notification.text && + notification.effect && + notification.status && + notification.startDateOfEffect && + notification.expectedEndDateOfEffect + +type DemandCapacityNotificationInformationModalProps = { + open: boolean; + demandCapacityNotification: DemandCapacityNotification | null; + onClose: () => void; + onSave: () => void; +}; + +type DemandCapacityNotificationViewProps = { + demandCapacityNotification: DemandCapacityNotification; + partners: Partner[] | null; +}; + +const DemandCapacityNotificationView = ({ demandCapacityNotification, partners }: DemandCapacityNotificationViewProps) => { + return + + + Text + {demandCapacityNotification.text} + + + Partner + {partners?.find((p) => p.bpnl === demandCapacityNotification.partnerBpnl)?.name} + + + Leading Root Cause + {LEADING_ROOT_CAUSE.find((dt) => dt.key === demandCapacityNotification.leadingRootCause)?.value} + + + Status + {STATUS.find((dt) => dt.key === demandCapacityNotification.status)?.value} + + + Effect + {EFFECTS.find((dt) => dt.key === demandCapacityNotification.effect)?.value} + + + Start Date of Effect + {new Date(demandCapacityNotification.startDateOfEffect).toLocaleString()} + + + Expexted End Date of Effect + {new Date(demandCapacityNotification.expectedEndDateOfEffect).toLocaleString()} + + + Affected Sites Sender + {demandCapacityNotification.affectedSitesBpnsSender && demandCapacityNotification.affectedSitesBpnsSender.length > 0 ? demandCapacityNotification.affectedSitesBpnsSender.join(", ") : 'None'} + + + Affected Sites Recipient + {demandCapacityNotification.affectedSitesBpnsRecipient && demandCapacityNotification.affectedSitesBpnsRecipient.length > 0 ? demandCapacityNotification.affectedSitesBpnsRecipient.join(", ") : 'None'} + + + Affected Material Numbers + {demandCapacityNotification.affectedMaterialNumbers && demandCapacityNotification.affectedMaterialNumbers.length > 0 ? demandCapacityNotification.affectedMaterialNumbers.join(", ") : 'None'} + + + +} + +export const DemandCapacityNotificationInformationModal = ({ open, demandCapacityNotification, onClose, onSave }: DemandCapacityNotificationInformationModalProps) => { + const [temporaryDemandCapacityNotification, setTemporaryDemandCapacityNotification] = useState>({}); + const { partners } = useAllPartners(); + const { partnerMaterials } = usePartnerMaterials(temporaryDemandCapacityNotification.partnerBpnl ?? `BPNL`); + + const [notifications, setNotifications] = useState([]); + const [formError, setFormError] = useState(false); + + const { sites } = useSites(); + + useEffect(() => { + setTemporaryDemandCapacityNotification(prevState => ({ + ...prevState, + affectedMaterialNumbers: [], + affectedSitesBpnsRecipient: [] + })); + }, [temporaryDemandCapacityNotification.partnerBpnl]); + + const handleSaveClick = () => { + if (!isValidDemandCapacityNotification(temporaryDemandCapacityNotification)) { + setFormError(true); + return; + } + setFormError(false); + postDemandAndCapacityNotification(temporaryDemandCapacityNotification) + .then(() => { + onSave(); + setNotifications((ns) => [ + ...ns, + { + title: 'Notification Added', + description: 'Notification has been added', + severity: 'success', + }, + ]); + }) + .catch((error) => { + setNotifications((ns) => [ + ...ns, + { + title: error.status === 409 ? 'Conflict' : 'Error requesting update', + description: error.status === 409 ? 'DemandCapacityNotification conflicting with an existing one' : error.error, + severity: 'error', + }, + ]); + }) + .finally(() => onClose()); + }; + + const handleClose = () => { + setFormError(false); + setTemporaryDemandCapacityNotification({}); + onClose(); + }; + return ( + <> + + + Demand Capacity Notification Information + + {!demandCapacityNotification ? + + + <> + + Text* +