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/demandandcapacitynotification/logic/service/OwnDemandAndCapacityNotificationService.java b/backend/src/main/java/org/eclipse/tractusx/puris/backend/demandandcapacitynotification/logic/service/OwnDemandAndCapacityNotificationService.java index 91a7c38c..6222fc93 100644 --- a/backend/src/main/java/org/eclipse/tractusx/puris/backend/demandandcapacitynotification/logic/service/OwnDemandAndCapacityNotificationService.java +++ b/backend/src/main/java/org/eclipse/tractusx/puris/backend/demandandcapacitynotification/logic/service/OwnDemandAndCapacityNotificationService.java @@ -42,12 +42,10 @@ public List findAllByPartnerBpnl(String bpnl) @Override public boolean validate(OwnDemandAndCapacityNotification notification) { return notification.getPartner() != null && - notification.getText() != null && notification.getLeadingRootCause() != null && notification.getEffect() != null && notification.getStatus() != null && notification.getStartDateOfEffect() != null && - notification.getExpectedEndDateOfEffect() != null && validateMaterials(notification) && validateSites(notification); } diff --git a/backend/src/main/java/org/eclipse/tractusx/puris/backend/demandandcapacitynotification/logic/service/ReportedDemandAndCapacityNotificationService.java b/backend/src/main/java/org/eclipse/tractusx/puris/backend/demandandcapacitynotification/logic/service/ReportedDemandAndCapacityNotificationService.java index 7aa2f7f8..da7b1f71 100644 --- a/backend/src/main/java/org/eclipse/tractusx/puris/backend/demandandcapacitynotification/logic/service/ReportedDemandAndCapacityNotificationService.java +++ b/backend/src/main/java/org/eclipse/tractusx/puris/backend/demandandcapacitynotification/logic/service/ReportedDemandAndCapacityNotificationService.java @@ -44,12 +44,10 @@ public List findAllByPartnerBpnl(String b @Override public boolean validate(ReportedDemandAndCapacityNotification notification) { return notification.getPartner() != null && - notification.getText() != null && notification.getLeadingRootCause() != null && notification.getEffect() != null && notification.getStatus() != null && notification.getStartDateOfEffect() != null && - notification.getExpectedEndDateOfEffect() != null && validateMaterials(notification) && validateSites(notification); } 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..3bdcc27f --- /dev/null +++ b/frontend/src/features/notifications/components/NotificationInformationModal.tsx @@ -0,0 +1,432 @@ +/* +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, Send } 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.effect && + notification.status && + notification.startDateOfEffect && + (!notification.expectedEndDateOfEffect || 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()} + + + Expected 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(handleClose); + }; + + const handleClose = () => { + setFormError(false); + setTemporaryDemandCapacityNotification({}); + onClose(); + }; + return ( + <> + + + Demand Capacity Notification Information + + + {!demandCapacityNotification ? ( + + <> + + Text +