diff --git a/CHANGELOG.md b/CHANGELOG.md index f074f605..ffe76fa7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,19 +4,28 @@ All notable changes to this project (both backend and frontend) will be document The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## [1.4.0] - [unreleased] +## [2.0.0] - [unreleased] ### Frontend -#### Fix -- Update Report and Table components from catena-x lib -- Configure css with new update components - #### Added +- Create `NegotiationPage` component to provide a user interface for triggering negotiations, displaying negotiation statuses, and managing catalog items. +- Implement `fetchCatalogItems` and `triggerNegotiation` service functions to interact with backend endpoints for retrieving catalog items and initiating negotiations. +- Introduce dynamic status icons in the negotiation table to reflect the real-time status of each negotiation, enhancing user feedback and interaction. - Added SnackBar for Report Table and Ratings for error and success messages +### Changed +- Update `UserInfo` component to conditionally display the negotiation page link in the user menu based on user roles, enhancing role-based access control. +- Modify the negotiation initiation process to reset item statuses to "Pending" before sending requests, providing clearer feedback on ongoing negotiations. +- Refine error handling in the negotiation process to alert users of failures and log errors for debugging purposes. + +### Fixed +- Resolve visual feedback issue where status icons would not reset to default state after re-initiating negotiations. +- Update Report and Table components from catena-x lib +- Configure css with new update components + + ## [1.3.2] - [2024-04-17] ### Backend diff --git a/backend/pom.xml b/backend/pom.xml index a00cb223..222c4e43 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -28,7 +28,7 @@ org.eclipse.tractusx value-added-service - 1.4.0 + 2.0.0 vas-country-risk-backend Project to Validate Country Risks Score diff --git a/frontend/package-lock.json b/frontend/package-lock.json index fdb8e530..a342501b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,15 +1,15 @@ { "name": "dashboard-app", - "version": "1.4.0", + "version": "2.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "dashboard-app", - "version": "1.4.0", + "version": "2.0.0", "license": "Apache-2.0", "dependencies": { - "@catena-x/portal-shared-components": "2.1.40", + "@catena-x/portal-shared-components": "^2.1.40", "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", "@mui/icons-material": "5.10.6", @@ -44,7 +44,7 @@ "@babel/preset-react": "^7.23.3", "babel-jest": "^29.7.0", "follow-redirects": ">=1.15.4", - "postcss": "^8.4.38" + "postcss": "^8.4.31" } }, "node_modules/@aashutoshrathi/word-wrap": { diff --git a/frontend/package.json b/frontend/package.json index fe424d6f..da199204 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,11 +1,11 @@ { "name": "dashboard-app", - "version": "1.4.0", + "version": "2.0.0", "license": "Apache-2.0", "private": true, "dependencies": { "react-dropzone": "^14.2.3", - "@catena-x/portal-shared-components": "2.1.40", + "@catena-x/portal-shared-components": "^2.1.40", "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", "@mui/icons-material": "5.10.6", @@ -70,7 +70,7 @@ "@babel/preset-react": "^7.23.3", "babel-jest": "^29.7.0", "follow-redirects": ">=1.15.4", - "postcss": "^8.4.38" + "postcss": "^8.4.31" }, "overrides": { "react-simple-maps": { @@ -81,7 +81,7 @@ "@svgr/webpack": "7.0.0" }, "resolve-url-loader": { - "postcss": "^8.4.38" + "postcss": "^8.4.31" }, "eslint": "8.32.0", "keycloak-js": "20.0.5", diff --git a/frontend/src/App.js b/frontend/src/App.js index 78a43f7c..20b2e3a4 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -33,6 +33,7 @@ import AboutPage from "./components/dashboard/AboutCard/AboutPage"; import { FooterPortal } from "./components/dashboard/Footer/FooterPortal"; import ErrorPageCR from "./components/dashboard/ErrorPage/ErrorPageCR"; import SignOut from "./components/dashboard/SignOut"; +import NegotiationPage from "./components/dashboard/NegotiationPage/NegotiationPage"; function App() { return ( @@ -53,6 +54,10 @@ function App() { } /> } /> } /> + } + /> diff --git a/frontend/src/components/dashboard/NavigationBar/UserInformation/UserInfo.js b/frontend/src/components/dashboard/NavigationBar/UserInformation/UserInfo.js index 468b2b45..9d546042 100644 --- a/frontend/src/components/dashboard/NavigationBar/UserInformation/UserInfo.js +++ b/frontend/src/components/dashboard/NavigationBar/UserInformation/UserInfo.js @@ -18,25 +18,25 @@ * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -import { useRef, useState } from "react"; +import { useState, useRef, useContext } from "react"; import { UserAvatar, UserMenu, UserNav, LanguageSwitch, } from "@catena-x/portal-shared-components"; -import LogoutIcon from "@mui/icons-material/Logout"; import UserService from "../../../services/UserService"; -import SignOut from "../../SignOut/index"; import "./UserInfo.scss"; import { getLogoutLink, - getAboutLink, + getNegotiationLink, } from "../../../services/EnvironmentService"; +import { CompanyUserContext } from "../../../../contexts/companyuser"; export const UserInfo = () => { const [menuOpen, setMenuOpen] = useState(false); const wrapperRef = useRef(null); + const { companyUser } = useContext(CompanyUserContext); const openCloseMenu = () => setMenuOpen((prevVal) => !prevVal); @@ -48,6 +48,28 @@ export const UserInfo = () => { const logoutHref = getLogoutLink(); + // Dynamically construct menu items based on user roles + let menuItems = [ + { + href: logoutHref, + title: "Logout", + }, + ]; + + // Add negotiation link for users with Negotiator or Admin roles + if ( + companyUser.roles.includes("Negotiator") || + companyUser.roles.includes("Admin") + ) { + menuItems = [ + { + href: getNegotiationLink(), + title: "Negotiation", + }, + ...menuItems, + ]; + } + return (
@@ -60,16 +82,10 @@ export const UserInfo = () => { userRole={UserService.getCompany()} onClickAway={onClickAway} > - +
); }; + +export default UserInfo; diff --git a/frontend/src/components/dashboard/NegotiationPage/NegotiationPage.js b/frontend/src/components/dashboard/NegotiationPage/NegotiationPage.js new file mode 100644 index 00000000..37caf010 --- /dev/null +++ b/frontend/src/components/dashboard/NegotiationPage/NegotiationPage.js @@ -0,0 +1,215 @@ +/******************************************************************************** + * Copyright (c) 2022,2024 BMW Group AG + * Copyright (c) 2022,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 React, { useContext, useState, useEffect } from "react"; + +import { CompanyUserContext } from "../../../contexts/companyuser"; +import "./NegotiationPage.scss"; // Assuming you'll have specific styles +import { Table } from "@catena-x/portal-shared-components"; +import { ErrorPage } from "@catena-x/portal-shared-components"; +import UserService from "../../services/UserService"; +import { useNavigate } from "react-router-dom"; +import { + fetchCatalogItems, + triggerNegotiation, +} from "../../services/negotiation"; +import CheckCircleIcon from "@mui/icons-material/CheckCircle"; +import CancelIcon from "@mui/icons-material/Cancel"; + +const NegotiationPage = () => { + const { companyUser } = useContext(CompanyUserContext); + const navigate = useNavigate(); + const [catalogItems, setCatalogItems] = useState([]); + const [selectedItems, setSelectedItems] = useState([]); + + useEffect(() => { + // Fetch catalog items from the API + const loadCatalogItems = async () => { + try { + const items = await fetchCatalogItems(UserService.getToken()); + setCatalogItems(items); + } catch (error) { + console.error("Error fetching catalog items:", error); + } + }; + + loadCatalogItems(); + }, []); + + const columns = [ + { + field: "id", + headerName: "ID", + flex: 2.2, + }, + { + field: "provider", + headerName: "Provider", + flex: 1, + }, + { + field: "subject", + headerName: "Subject", + flex: 2, + }, + { + field: "description", + headerName: "Description", + flex: 2.5, + }, + { + field: "status", + headerName: "Status", + flex: 1, + renderCell: (params) => { + switch (params.value) { + case "Error": + return ; + case "Success": + return ; + case "Pending": + // Optionally, return a spinner or a transparent placeholder + return null; // For now, do nothing for pending state + default: + return null; // For initial state or any unrecognized status + } + }, + }, + ]; + + const handleInitiateNegotiation = async () => { + if (selectedItems.length === 0) { + alert("Please select at least one item to negotiate."); + return; + } + + // Reset the status of selected items to "Pending" before negotiation + const resetItems = catalogItems.map((item) => { + if (selectedItems.find((selectedItem) => selectedItem.id === item.id)) { + return { ...item, status: "Pending" }; // Assuming "Pending" means no icon displayed + } + return item; + }); + setCatalogItems(resetItems); // Update state to reflect the reset statuses + + const negotiationRequest = selectedItems.map((item) => ({ + id: item.id, + offerId: item.offerId, + })); + + try { + const response = await triggerNegotiation( + negotiationRequest, + UserService.getToken() + ); + // Update the status of catalog items based on negotiation response + updateItemStatuses(response); + } catch (error) { + console.error("Error initiating negotiation:", error); + alert("Failed to initiate negotiation."); + } + }; + + // Utility function to update the status of catalog items based on negotiation response + const updateItemStatuses = (negotiationResults) => { + const updatedItems = catalogItems.map((item) => { + const negotiationResult = negotiationResults.find( + (result) => result.id === item.id + ); + if (negotiationResult) { + return { + ...item, + status: negotiationResult.status === "Success" ? "Success" : "Error", + }; + } + return item; // Return item unchanged if no negotiation result is found + }); + + setCatalogItems(updatedItems); // Update state with the new items including their statuses + }; + + // Check if the user has permission to initiate negotiations + const userCanNegotiate = + companyUser.roles.includes("Negotiator") || + companyUser.roles.includes("Admin"); // Adjust the role check as necessary + + return ( +
+ {userCanNegotiate ? ( + <> +

Catalog Items Available for Negotiation

+
+ = 5 ? 5 : catalogItems.length} + autoHeight={true} + initialState={{ + pagination: { + paginationModel: { + pageSize: 5, + }, + }, + }} + pageSizeOptions={[5]} + checkboxSelection + disableColumnMenu={true} + columnHeadersBackgroundColor="rgb(233, 233, 233)" + onRowSelectionModelChange={(ids) => { + // Assuming ids are the indexes in the catalogItems array + const selectedIDs = new Set(ids); // If ids are directly the item IDs, you may not need mapping + const selectedCatalogItems = catalogItems.filter( + (item) => selectedIDs.has(item.id.toString()) // Ensure the comparison works by matching types + ); + setSelectedItems(selectedCatalogItems); + }} + title="Available Items" + toolbarVariant="basic" + buttonLabel="Start Negotiation" + onButtonClick={handleInitiateNegotiation} + experimentalFeatures={{ newEditingApi: true }} + hideFooter={catalogItems.length <= 5} + /> + + + ) : ( + { + window.location.href = navigate("/"); + }} + onReloadClick={() => { + navigate("/"); + }} + reloadButtonTitle="Reload Page" + title="Oops, Something went wrong." + /> + )} + + ); +}; + +export default NegotiationPage; diff --git a/frontend/src/components/dashboard/NegotiationPage/NegotiationPage.scss b/frontend/src/components/dashboard/NegotiationPage/NegotiationPage.scss new file mode 100644 index 00000000..39f9e900 --- /dev/null +++ b/frontend/src/components/dashboard/NegotiationPage/NegotiationPage.scss @@ -0,0 +1,54 @@ +/******************************************************************************** +* Copyright (c) 2022,2024 BMW Group AG +* Copyright (c) 2022,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 +********************************************************************************/ +.negotiation-page { + padding: 2% 5%; + background-color: #f5f5f5; // Light grey background for slight contrast + min-height: 100vh; // Ensures it takes up at least the full height of the viewport + .table { + background-color: white; + font-size: 10px !important; + } + .description-cell { + max-width: 250px; // Adjust based on your layout + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .table-style { + padding-top: 2%; + max-width: 85%; // Reducing the maximum width of the table + width: 100%; // Ensures the table width scales with the container + margin: 0 auto; // Centers the table horizontally + } + + h1 { + color: #333; + text-align: center; + margin-bottom: 20px; + } + + // Styles for the error page layout + .error-page-container { + text-align: center; + padding: 50px; + color: #dc3545; // Example using Bootstrap's danger color + } +} diff --git a/frontend/src/components/services/EnvironmentService.js b/frontend/src/components/services/EnvironmentService.js index e86e71b9..1e7058ff 100644 --- a/frontend/src/components/services/EnvironmentService.js +++ b/frontend/src/components/services/EnvironmentService.js @@ -53,6 +53,10 @@ export const getLogoutLink = () => { return document.location.origin + "/logout"; }; +export const getNegotiationLink = () => { + return document.location.origin + "/negotiation"; +}; + export const getAboutLink = () => { return LOCAL_SERVICES_FRONTEND + "/about"; }; diff --git a/frontend/src/components/services/negotiation.js b/frontend/src/components/services/negotiation.js new file mode 100644 index 00000000..91459265 --- /dev/null +++ b/frontend/src/components/services/negotiation.js @@ -0,0 +1,62 @@ +/******************************************************************************** + * Copyright (c) 2022,2024 BMW Group AG + * Copyright (c) 2022,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 axios from "axios"; +import { getCountryRiskApi } from "./EnvironmentService"; + +const API_BASE_URL = getCountryRiskApi(); // Update with your actual base URL + +// Function to fetch catalog items +export const fetchCatalogItems = async (authToken) => { + try { + const response = await axios.get( + `${API_BASE_URL}` + process.env.REACT_APP_GET_QUERY_CATALOG, + { + headers: { + Authorization: `Bearer ${authToken}`, // Assuming bearer token is used for auth + "Content-Type": "application/json", + }, + } + ); + return response.data; + } catch (error) { + console.error("Failed to fetch catalog items:", error); + throw error; + } +}; + +// Function to trigger negotiation with selected items +export const triggerNegotiation = async (selectedItems, authToken) => { + try { + const response = await axios.post( + `${API_BASE_URL}` + process.env.REACT_APP_POST_TRIGGER_NEGOTIATION, + selectedItems, + { + headers: { + Authorization: `Bearer ${authToken}`, // Assuming bearer token is used for auth + "Content-Type": "application/json", + }, + } + ); + return response.data; + } catch (error) { + console.error("Failed to trigger negotiation:", error); + throw error; + } +};