@@ -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;
+ }
+};