From 569c96e57d8ddff18b10c6547aef7a3a10fed9ce Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ren=C3=A9=20Schr=C3=B6der?=
<131770181+ReneSchroederLJ@users.noreply.github.com>
Date: Wed, 17 Apr 2024 12:56:23 +0200
Subject: [PATCH 1/3] feat: implementation of short term material demand in the
frontend
---
DEPENDENCIES_BACKEND | 6 +-
backend/DEPENDENCIES | 6 +-
frontend/.env | 1 +
frontend/.env.dockerbuild | 1 +
frontend/nginx.conf | 1 +
.../src/components/TableWithRowHeader.tsx | 3 +-
.../dashboard/components/Dashboard.tsx | 242 +++++++----
.../dashboard/components/DashboardFilters.tsx | 2 +-
.../components/DemandCategoryModal.tsx | 385 ++++++++++++++++++
.../dashboard/components/DemandTable.tsx | 124 ++++--
.../src/features/dashboard/hooks/useDemand.ts | 34 ++
.../dashboard/hooks/useReportedDemand.ts | 13 +
.../src/features/dashboard/util/helpers.tsx | 27 +-
frontend/src/hooks/useDemand.ts | 34 ++
frontend/src/index.css | 8 +-
frontend/src/models/constants/config.ts | 1 +
.../src/models/constants/demand-category.ts | 54 +++
.../src/models/types/data/demand-category.ts | 27 ++
frontend/src/models/types/data/demand.ts | 36 ++
.../src/models/types/data/notification.ts | 25 ++
frontend/src/services/demands-service.ts | 52 +++
frontend/src/util/helpers.ts | 1 -
22 files changed, 959 insertions(+), 124 deletions(-)
create mode 100644 frontend/src/features/dashboard/components/DemandCategoryModal.tsx
create mode 100644 frontend/src/features/dashboard/hooks/useDemand.ts
create mode 100644 frontend/src/features/dashboard/hooks/useReportedDemand.ts
create mode 100644 frontend/src/hooks/useDemand.ts
create mode 100644 frontend/src/models/constants/demand-category.ts
create mode 100644 frontend/src/models/types/data/demand-category.ts
create mode 100644 frontend/src/models/types/data/demand.ts
create mode 100644 frontend/src/models/types/data/notification.ts
create mode 100644 frontend/src/services/demands-service.ts
diff --git a/DEPENDENCIES_BACKEND b/DEPENDENCIES_BACKEND
index b8eb1e37..d5be54a6 100644
--- a/DEPENDENCIES_BACKEND
+++ b/DEPENDENCIES_BACKEND
@@ -45,9 +45,9 @@ maven/mavencentral/org.assertj/assertj-core/3.24.2, Apache-2.0, approved, #6161
maven/mavencentral/org.awaitility/awaitility/4.2.0, Apache-2.0, approved, #14178
maven/mavencentral/org.checkerframework/checker-qual/3.42.0, MIT, approved, clearlydefined
maven/mavencentral/org.eclipse.angus/angus-activation/2.0.1, EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.angus
-maven/mavencentral/org.glassfish.jaxb/jaxb-core/4.0.4, BSD-3-Clause, approved, ee4j.jaxb
-maven/mavencentral/org.glassfish.jaxb/jaxb-runtime/4.0.4, BSD-3-Clause, approved, ee4j.jaxb
-maven/mavencentral/org.glassfish.jaxb/txw2/4.0.4, BSD-3-Clause, approved, ee4j.jaxb
+maven/mavencentral/org.glassfish.jaxb/jaxb-core/4.0.4, BSD-3-Clause, approved, ee4j.jaxb-impl
+maven/mavencentral/org.glassfish.jaxb/jaxb-runtime/4.0.4, BSD-3-Clause, approved, ee4j.jaxb-impl
+maven/mavencentral/org.glassfish.jaxb/txw2/4.0.4, BSD-3-Clause, approved, ee4j.jaxb-impl
maven/mavencentral/org.hamcrest/hamcrest/2.2, BSD-3-Clause, approved, clearlydefined
maven/mavencentral/org.hibernate.common/hibernate-commons-annotations/6.0.6.Final, LGPL-2.1-only, approved, #6962
maven/mavencentral/org.hibernate.orm/hibernate-core/6.4.1.Final, LGPL-2.1-or-later AND (EPL-2.0 OR BSD-3-Clause) AND MIT, approved, #12490
diff --git a/backend/DEPENDENCIES b/backend/DEPENDENCIES
index b8eb1e37..d5be54a6 100644
--- a/backend/DEPENDENCIES
+++ b/backend/DEPENDENCIES
@@ -45,9 +45,9 @@ maven/mavencentral/org.assertj/assertj-core/3.24.2, Apache-2.0, approved, #6161
maven/mavencentral/org.awaitility/awaitility/4.2.0, Apache-2.0, approved, #14178
maven/mavencentral/org.checkerframework/checker-qual/3.42.0, MIT, approved, clearlydefined
maven/mavencentral/org.eclipse.angus/angus-activation/2.0.1, EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.angus
-maven/mavencentral/org.glassfish.jaxb/jaxb-core/4.0.4, BSD-3-Clause, approved, ee4j.jaxb
-maven/mavencentral/org.glassfish.jaxb/jaxb-runtime/4.0.4, BSD-3-Clause, approved, ee4j.jaxb
-maven/mavencentral/org.glassfish.jaxb/txw2/4.0.4, BSD-3-Clause, approved, ee4j.jaxb
+maven/mavencentral/org.glassfish.jaxb/jaxb-core/4.0.4, BSD-3-Clause, approved, ee4j.jaxb-impl
+maven/mavencentral/org.glassfish.jaxb/jaxb-runtime/4.0.4, BSD-3-Clause, approved, ee4j.jaxb-impl
+maven/mavencentral/org.glassfish.jaxb/txw2/4.0.4, BSD-3-Clause, approved, ee4j.jaxb-impl
maven/mavencentral/org.hamcrest/hamcrest/2.2, BSD-3-Clause, approved, clearlydefined
maven/mavencentral/org.hibernate.common/hibernate-commons-annotations/6.0.6.Final, LGPL-2.1-only, approved, #6962
maven/mavencentral/org.hibernate.orm/hibernate-core/6.4.1.Final, LGPL-2.1-or-later AND (EPL-2.0 OR BSD-3-Clause) AND MIT, approved, #12490
diff --git a/frontend/.env b/frontend/.env
index 9d9c5d7d..611b1a3f 100644
--- a/frontend/.env
+++ b/frontend/.env
@@ -13,6 +13,7 @@ VITE_ENDPOINT_REPORTED_PRODUCT_STOCKS=stockView/reported-product-stocks?ownMater
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_PARTNER_OWNSITES=partners/ownSites
+VITE_ENDPOINT_DEMAND=demand
VITE_IDP_DISABLE=true
VITE_IDP_URL=http://localhost:10081/
diff --git a/frontend/.env.dockerbuild b/frontend/.env.dockerbuild
index 91f0ed29..07d62fe9 100644
--- a/frontend/.env.dockerbuild
+++ b/frontend/.env.dockerbuild
@@ -12,6 +12,7 @@ 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_PARTNER_OWNSITES=\$ENDPOINT_PARTNER_OWNSITES
+VITE_ENDPOINT_DEMAND=\$ENDPOINT_DEMAND
VITE_IDP_DISABLE=\$IDP_DISABLE
VITE_IDP_URL=\$IDP_URL
diff --git a/frontend/nginx.conf b/frontend/nginx.conf
index 586abca8..38c7f681 100755
--- a/frontend/nginx.conf
+++ b/frontend/nginx.conf
@@ -41,6 +41,7 @@ http {
limit_req zone=zoneLimit burst=${NGINX_BURST} nodelay;
root /usr/share/nginx/html;
index index.html index.htm;
+ try_files $uri $uri/ /index.html;
}
}
}
diff --git a/frontend/src/components/TableWithRowHeader.tsx b/frontend/src/components/TableWithRowHeader.tsx
index e18e2c58..edb3e8bd 100644
--- a/frontend/src/components/TableWithRowHeader.tsx
+++ b/frontend/src/components/TableWithRowHeader.tsx
@@ -31,6 +31,7 @@ export const TableWithRowHeader = ({ rows, ...tableProps }: TableWithRowHeaderPr
title=''
columns={[{ field: 'name', headerName: '', width: 180 }]}
rows={rows}
+ density='standard'
rowSelection={false}
hideFooter={true}
disableColumnFilter
@@ -38,7 +39,7 @@ export const TableWithRowHeader = ({ rows, ...tableProps }: TableWithRowHeaderPr
sortingMode={'server'}
/>
-
+
diff --git a/frontend/src/features/dashboard/components/Dashboard.tsx b/frontend/src/features/dashboard/components/Dashboard.tsx
index 9967634b..2729afd8 100644
--- a/frontend/src/features/dashboard/components/Dashboard.tsx
+++ b/frontend/src/features/dashboard/components/Dashboard.tsx
@@ -22,98 +22,204 @@ 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 { useState } from 'react';
+import { useCallback, useReducer } from 'react';
import { DashboardFilters } from './DashboardFilters';
import { DemandTable } from './DemandTable';
import { ProductionTable } from './ProductionTable';
-import { Stack, Typography, capitalize } from '@mui/material';
+import { Box, Button, Stack, Typography, capitalize } from '@mui/material';
import { Delivery } from '@models/types/data/delivery';
import { DeliveryInformationModal } from './DeliveryInformationModal';
import { getPartnerType } from '../util/helpers';
+import { Demand } from '@models/types/data/demand';
+import { DemandCategoryModal } from './DemandCategoryModal';
+import { DEMAND_CATEGORY } from '@models/constants/demand-category';
+import { LoadingButton } from '@catena-x/portal-shared-components';
+import { Refresh } from '@mui/icons-material';
+import { useDemand } from '../hooks/useDemand';
+import { useReportedDemand } from '../hooks/useReportedDemand';
+import { refreshPartnerStocks } from '@services/stocks-service';
-const NUMBER_OF_DAYS = 42;
+const NUMBER_OF_DAYS = 28;
+
+type DashboardState = {
+ selectedMaterial: MaterialDescriptor | null;
+ selectedSite: Site | null;
+ selectedPartnerSites: Site[] | null;
+ deliveryDialogOptions: { open: boolean; mode: 'create' | 'edit' };
+ demandDialogOptions: { open: boolean; mode: 'create' | 'edit' };
+ productionDialogOptions: { open: boolean; mode: 'create' | 'edit' };
+ delivery: Delivery | null;
+ demand: Partial | null;
+ isRefreshing: boolean;
+};
+
+type DashboardAction = {
+ type: keyof DashboardState;
+ payload: DashboardState[keyof DashboardState];
+};
+
+const reducer = (state: DashboardState, action: DashboardAction): DashboardState => {
+ return { ...state, [action.type]: action.payload };
+};
+
+const initialState: DashboardState = {
+ selectedMaterial: null,
+ selectedSite: null,
+ selectedPartnerSites: null,
+ deliveryDialogOptions: { open: false, mode: 'create' },
+ demandDialogOptions: { open: false, mode: 'edit' },
+ productionDialogOptions: { open: false, mode: 'edit' },
+ delivery: null,
+ demand: null,
+ isRefreshing: false,
+};
export const Dashboard = ({ type }: { type: 'customer' | 'supplier' }) => {
- const [selectedMaterial, setSelectedMaterial] = useState(null);
- const [selectedSite, setSelectedSite] = useState(null);
- const [selectedPartnerSites, setSelectedPartnerSites] = useState(null);
+ const [state, dispatch] = useReducer(reducer, initialState);
const { stocks } = useStocks(type === 'customer' ? 'material' : 'product');
- const { partnerStocks } = usePartnerStocks(type === 'customer' ? 'material' : 'product', selectedMaterial?.ownMaterialNumber ?? null);
- const [open, setOpen] = useState(false);
- const [delivery, setDelivery] = useState(null);
- const openDeliveryDialog = (d: Delivery) => {
- setDelivery(d);
- setOpen(true);
+ const { partnerStocks, refreshPartnerStocks: refresh } = usePartnerStocks(
+ type === 'customer' ? 'material' : 'product',
+ state.selectedMaterial?.ownMaterialNumber ?? null
+ );
+ const { demands, refreshDemand } = useDemand(state.selectedMaterial?.ownMaterialNumber ?? null, state.selectedSite?.bpns ?? null);
+ const { reportedDemands } = useReportedDemand(state.selectedMaterial?.ownMaterialNumber ?? null);
+
+ const handleRefresh = () => {
+ dispatch({ type: 'isRefreshing', payload: true });
+ refreshPartnerStocks( type === 'customer' ? 'material' : 'product', state.selectedMaterial?.ownMaterialNumber ?? null )
+ .then(refresh)
+ .finally(() => dispatch({ type: 'isRefreshing', payload: false }));
};
- const handleMaterialSelect = (material: MaterialDescriptor | null) => {
- setSelectedMaterial(material);
- setSelectedSite(null);
- setSelectedPartnerSites(null);
+ const openDeliveryDialog = (d: Partial) => {
+ dispatch({ type: 'delivery', payload: d });
+ dispatch({ type: 'deliveryDialogOptions', payload: { open: true, mode: 'edit' } });
+ };
+ const handleMaterialSelect = useCallback((material: MaterialDescriptor | null) => {
+ dispatch({ type: 'selectedMaterial', payload: material });
+ dispatch({ type: 'selectedSite', payload: null });
+ dispatch({ type: 'selectedPartnerSites', payload: null });
+ }, []);
+ const openDemandDialog = (d: Partial, mode: 'create' | 'edit') => {
+ d.measurementUnit ??= 'unit:piece';
+ d.demandCategoryCode ??= DEMAND_CATEGORY[0]?.key;
+ dispatch({ type: 'demand', payload: d });
+ dispatch({ type: 'demandDialogOptions', payload: { open: true, mode } });
};
return (
<>
-
+
dispatch({ type: 'selectedSite', payload: site })}
+ onPartnerSitesChange={(sites) => dispatch({ type: 'selectedPartnerSites', payload: sites })}
/>
-
- Our Stock Information {selectedMaterial && selectedSite && <>for {selectedMaterial.description}>}
-
- {selectedSite ? (
- type === 'supplier' ? (
-
+
+
+ Production Information
+ {state.selectedMaterial && state.selectedSite && <> for {state.selectedMaterial.description} ({state.selectedMaterial.ownMaterialNumber})>}
+
+ {state.selectedSite && state.selectedMaterial ? (
+ type === 'supplier' ? (
+
+ ) : (
+
+ )
) : (
-
- )
- ) : (
- Select a Site to show production data
- )}
- {selectedSite && (
- <>
-
- {`${capitalize(getPartnerType(type))} Stocks ${selectedMaterial ? `for ${selectedMaterial?.description}` : ''}`}
-
- {selectedPartnerSites ? (
- selectedPartnerSites.map((ps) =>
- type === 'supplier' ? (
- Select a Site to show production data
+ )}
+
+ {state.selectedSite && (
+
+
+
+ {`${capitalize(getPartnerType(type))} Information ${
+ state.selectedMaterial ? `for ${state.selectedMaterial.description} (${state.selectedMaterial.ownMaterialNumber})` : ''
+ }`}
+
+ {state.selectedPartnerSites?.length &&
+ (state.isRefreshing ? (
+
) : (
-
+
+ ))}
+
+
+ {state.selectedPartnerSites ? (
+ state.selectedPartnerSites.map((ps) =>
+ type === 'supplier' ? (
+ d.demandLocationBpns === ps.bpns) ?? []}
+ readOnly
+ />
+ ) : (
+
+ )
)
- )
- ) : (
- {`Select a ${getPartnerType(type)} site to show their stock information`}
- )}
- >
+ ) : (
+ {`Select a ${getPartnerType(
+ type
+ )} site to show their stock information`}
+ )}
+
+
)}
- setOpen(false)} delivery={delivery} />
+
+ dispatch({ type: 'deliveryDialogOptions', payload: { open: false, mode: state.deliveryDialogOptions.mode } })
+ }
+ delivery={state.delivery}
+ />
+ dispatch({ type: 'demandDialogOptions', payload: { open: false, mode: state.demandDialogOptions.mode } })}
+ onSave={refreshDemand}
+ demand={state.demand}
+ demands={demands ?? []}
+ />
>
);
};
diff --git a/frontend/src/features/dashboard/components/DashboardFilters.tsx b/frontend/src/features/dashboard/components/DashboardFilters.tsx
index d639694b..a72d5698 100644
--- a/frontend/src/features/dashboard/components/DashboardFilters.tsx
+++ b/frontend/src/features/dashboard/components/DashboardFilters.tsx
@@ -56,7 +56,7 @@ export const DashboardFilters = ({
id="material"
value={material}
options={materials ?? []}
- getOptionLabel={(option) => option.ownMaterialNumber}
+ getOptionLabel={(option) => `${option.description} (${option.ownMaterialNumber})`}
renderInput={(params) => }
onChange={(_, newValue) => onMaterialChange(newValue || null)}
/>
diff --git a/frontend/src/features/dashboard/components/DemandCategoryModal.tsx b/frontend/src/features/dashboard/components/DemandCategoryModal.tsx
new file mode 100644
index 00000000..a0b299ff
--- /dev/null
+++ b/frontend/src/features/dashboard/components/DemandCategoryModal.tsx
@@ -0,0 +1,385 @@
+/*
+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 { useCallback, useEffect, useMemo, useState } from 'react';
+import { Datepicker, Input, PageSnackbar, PageSnackbarStack, Table } from '@catena-x/portal-shared-components';
+import { UNITS_OF_MEASUREMENT } from '@models/constants/uom';
+import { Demand } from '@models/types/data/demand';
+import { Autocomplete, Box, Button, Dialog, DialogTitle, Grid, Stack, Typography } from '@mui/material';
+import { getUnitOfMeasurement } from '@util/helpers';
+import { usePartners } from '@features/stock-view/hooks/usePartners';
+import { Notification } from '@models/types/data/notification';
+import { deleteDemand, postDemand } from '@services/demands-service';
+import { DEMAND_CATEGORY } from '@models/constants/demand-category';
+import { Close, Delete, Save } from '@mui/icons-material';
+
+const GridItem = ({ label, value }: { label: string; value: string }) => (
+
+
+
+ {label}:
+
+
+ {value}
+
+
+
+);
+
+const createDemandColumns = (handleDelete: (row: Demand) => void) => [
+ {
+ field: 'quantity',
+ headerName: 'Quantity',
+ sortable: false,
+ disableColumnMenu: true,
+ headerAlign: 'center',
+ type: 'string',
+ width: 120,
+ renderCell: (data: { row: Demand }) => {
+ return (
+
+ {`${data.row.quantity} ${getUnitOfMeasurement(data.row.measurementUnit)}`}
+
+ );
+ },
+ },
+ {
+ field: 'partner',
+ headerName: 'Partner',
+ sortable: false,
+ disableColumnMenu: true,
+ headerAlign: 'center',
+ type: 'string',
+ width: 200,
+ renderCell: (data: { row: Demand }) => {
+ return (
+
+ {data.row.partnerBpnl}
+
+ );
+ },
+ },
+ {
+ field: 'supplierLocationBpns',
+ headerName: 'Expected Supplier Location',
+ sortable: false,
+ disableColumnMenu: true,
+ headerAlign: 'center',
+ type: 'string',
+ width: 200,
+ renderCell: (data: { row: Demand }) => {
+ return (
+
+ {data.row.supplierLocationBpns}
+
+ );
+ },
+ },
+ {
+ field: 'category',
+ headerName: 'Demand Category',
+ sortable: false,
+ disableColumnMenu: true,
+ headerAlign: 'center',
+ type: 'string',
+ width: 200,
+ renderCell: (data: { row: Demand }) => {
+ return (
+
+ {DEMAND_CATEGORY.find(cat => cat.key === data.row.demandCategoryCode)?.value ?? DEMAND_CATEGORY[0].value}
+
+ );
+ },
+ },
+ {
+ field: 'delete',
+ headerName: '',
+ sortable: false,
+ disableColumnMenu: true,
+ headerAlign: 'center',
+ type: 'string',
+ width: 30,
+ renderCell: (data: { row: Demand }) => {
+ return (
+
+
+
+ );
+ },
+ },
+] as const;
+
+type DemandCategoryModalProps = {
+ open: boolean;
+ demand: Partial | null;
+ demands: Demand[] | null;
+ mode: 'create' | 'edit';
+ onClose: () => void;
+ onSave: () => void;
+};
+
+const isValidDemand = (demand: Partial) =>
+ demand?.day &&
+ demand?.quantity &&
+ demand.demandCategoryCode &&
+ demand?.measurementUnit &&
+ demand?.partnerBpnl &&
+ demand?.supplierLocationBpns;
+
+export const DemandCategoryModal = ({ open, mode, onClose, onSave, demand, demands }: DemandCategoryModalProps) => {
+ const [temporaryDemand, setTemporaryDemand] = useState>(demand ?? {});
+ const { partners } = usePartners('material', temporaryDemand?.ownMaterialNumber ?? null);
+ const [notifications, setNotifications] = useState([]);
+ const [formError, setFormError] = useState(false);
+ const dailyDemands = useMemo(
+ () =>
+ demands?.filter(
+ (d) =>
+ d.day && (new Date(d.day).toLocaleDateString() ===
+ new Date(demand?.day ?? Date.now()).toLocaleDateString())
+ ),
+ [demands, demand?.day]
+ );
+
+ const handleSaveClick = useCallback(
+ (demand: Partial) => {
+ if (!isValidDemand(demand)) {
+ setFormError(true);
+ return;
+ }
+ setFormError(false);
+ postDemand(demand)
+ .then(() => {
+ onSave();
+ setNotifications((ns) => [
+ ...ns,
+ {
+ title: 'Demand Created',
+ description: 'The Demand has been saved successfully',
+ severity: 'success',
+ },
+ ]);
+ })
+ .catch((error) => {
+ setNotifications((ns) => [
+ ...ns,
+ {
+ title: error.status === 409 ? 'Conflict' : 'Error requesting update',
+ description: error.status === 409 ? 'Date conflicting with another Demand' : error.error,
+ severity: 'error',
+ },
+ ]);
+ })
+ .finally(onClose);
+ },
+ [onClose, onSave]
+ );
+ const handleDelete = (row: Demand) => {
+ if (row.uuid) deleteDemand(row.uuid).then(onSave);
+ };
+ useEffect(() => {
+ if (demand) {
+ setTemporaryDemand(demand);
+ }
+ }, [demand]);
+ return (
+ <>
+