diff --git a/backend/src/main/java/org/eclipse/tractusx/puris/backend/production/logic/service/OwnProductionService.java b/backend/src/main/java/org/eclipse/tractusx/puris/backend/production/logic/service/OwnProductionService.java
index 2f119f52..fc3ba137 100644
--- a/backend/src/main/java/org/eclipse/tractusx/puris/backend/production/logic/service/OwnProductionService.java
+++ b/backend/src/main/java/org/eclipse/tractusx/puris/backend/production/logic/service/OwnProductionService.java
@@ -126,11 +126,10 @@ public boolean validate(OwnProduction production) {
ownPartnerEntity.getSites().stream().anyMatch(site -> site.getBpns().equals(production.getProductionSiteBpns())) &&
((
production.getCustomerOrderNumber() != null &&
- production.getCustomerOrderPositionNumber() != null &&
- production.getSupplierOrderNumber() != null
+ production.getCustomerOrderPositionNumber() != null
) || (
production.getCustomerOrderNumber() == null &&
- production.getCustomerOrderPositionNumber() == null &&
+ production.getCustomerOrderPositionNumber() == null &&
production.getSupplierOrderNumber() == null
));
}
diff --git a/backend/src/main/java/org/eclipse/tractusx/puris/backend/production/logic/service/ReportedProductionService.java b/backend/src/main/java/org/eclipse/tractusx/puris/backend/production/logic/service/ReportedProductionService.java
index c6188e55..4745e35c 100644
--- a/backend/src/main/java/org/eclipse/tractusx/puris/backend/production/logic/service/ReportedProductionService.java
+++ b/backend/src/main/java/org/eclipse/tractusx/puris/backend/production/logic/service/ReportedProductionService.java
@@ -116,11 +116,10 @@ public boolean validate(ReportedProduction production) {
production.getPartner().getSites().stream().anyMatch(site -> site.getBpns().equals(production.getProductionSiteBpns())) &&
((
production.getCustomerOrderNumber() != null &&
- production.getCustomerOrderPositionNumber() != null &&
- production.getSupplierOrderNumber() != null
+ production.getCustomerOrderPositionNumber() != null
) || (
production.getCustomerOrderNumber() == null &&
- production.getCustomerOrderPositionNumber() == null &&
+ production.getCustomerOrderPositionNumber() == null &&
production.getSupplierOrderNumber() == null
));
}
diff --git a/frontend/.env b/frontend/.env
index 9d9c5d7d..6d4c9128 100644
--- a/frontend/.env
+++ b/frontend/.env
@@ -13,6 +13,8 @@ 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_PRODUCTION=production
+VITE_ENDPOINT_PRODUCTION_RANGE=production/range
VITE_IDP_DISABLE=true
VITE_IDP_URL=http://localhost:10081/
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/components/layout/SideBar.tsx b/frontend/src/components/layout/SideBar.tsx
index 24b4e045..36607905 100644
--- a/frontend/src/components/layout/SideBar.tsx
+++ b/frontend/src/components/layout/SideBar.tsx
@@ -45,6 +45,11 @@ type SideBarItemProps = (
};
const sideBarItems: SideBarItemProps[] = [
+ {
+ name: 'Dashboard',
+ icon: HomeIcon,
+ path: '/dashboard',
+ },
{
name: 'Stocks',
icon: StockIcon,
@@ -68,11 +73,6 @@ const sideBarItems: SideBarItemProps[] = [
path: '/transfers',
requiredRoles: ['PURIS_ADMIN'],
},
- {
- name: 'Supplier Dashboard',
- icon: HomeIcon,
- path: '/supplierDashboard',
- },
{
name: 'Logout',
icon: TrashIcon,
diff --git a/frontend/src/components/ui/DateTime.tsx b/frontend/src/components/ui/DateTime.tsx
new file mode 100644
index 00000000..4c917f2c
--- /dev/null
+++ b/frontend/src/components/ui/DateTime.tsx
@@ -0,0 +1,99 @@
+/*
+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 { DateType, Datepicker } from '@catena-x/portal-shared-components';
+import Box from '@mui/material/Box';
+import { useEffect, useRef, useState } from 'react';
+
+const isValidTime = (time?: string) => {
+ if (!time) {
+ return false;
+ }
+ const splits = time.split(':');
+ if (splits.length !== 2) {
+ return false;
+ }
+ const [hours, minutes] = splits;
+ return parseInt(hours) >= 0 && parseInt(hours) <= 23 && parseInt(minutes) >= 0 && parseInt(minutes) <= 59;
+};
+
+type DateTimeProps = {
+ label: string;
+ placeholder: string;
+ locale: 'en' | 'de';
+ error: boolean;
+ value: Date | null;
+ onValueChange: (date: Date | null) => void;
+};
+
+export const DateTime = ({ error, value, onValueChange, ...props }: DateTimeProps) => {
+ const [date, setDate] = useState(value ? new Date(value) : null);
+ const timeRef = useRef(null);
+ const handleTimeChange = () => {
+ const time = timeRef.current?.value;
+ if (time && date) {
+ const [hours, minutes] = time.split(':');
+ const newDate = new Date(date);
+ newDate.setHours(parseInt(hours));
+ newDate.setMinutes(parseInt(minutes));
+ onValueChange(newDate);
+ } else {
+ onValueChange(null);
+ }
+ };
+ const handleDateChange = (newDate: DateType) => {
+ setDate(newDate);
+ if (newDate && timeRef.current) {
+ onValueChange(new Date(newDate));
+ } else {
+ onValueChange(null);
+ }
+ };
+
+ useEffect(() => {
+ if (value) {
+ const d = new Date(value);
+ setDate(d);
+ const hours = d.getHours().toString().padStart(2, '0');
+ const minutes = d.getMinutes().toString().padStart(2, '0');
+ timeRef.current!.value = `${hours}:${minutes}`;
+ }
+ }, [value]);
+ return (
+
+
+ handleDateChange(event)}
+ />
+
+
+
+ );
+};
diff --git a/frontend/src/features/dashboard/components/Dashboard.tsx b/frontend/src/features/dashboard/components/Dashboard.tsx
index 9967634b..787a3feb 100644
--- a/frontend/src/features/dashboard/components/Dashboard.tsx
+++ b/frontend/src/features/dashboard/components/Dashboard.tsx
@@ -17,103 +17,215 @@ under the License.
SPDX-License-Identifier: Apache-2.0
*/
-
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 { Production } from '@models/types/data/production';
+import { PlannedProductionModal } from './PlannedProductionModal';
+import { useProduction } from '../hooks/useProduction';
+import { useReportedProduction } from '../hooks/useReportedProduction';
+import { LoadingButton } from '@catena-x/portal-shared-components';
+import { Refresh } from '@mui/icons-material';
+import { refreshPartnerStocks } from '@services/stocks-service';
+
+const NUMBER_OF_DAYS = 28;
-const NUMBER_OF_DAYS = 42;
+type DashboardState = {
+ selectedMaterial: MaterialDescriptor | null;
+ selectedSite: Site | null;
+ selectedPartnerSites: Site[] | null;
+ deliveryDialogOptions: { open: boolean; mode: 'create' | 'edit' };
+ productionDialogOptions: { open: boolean; mode: 'create' | 'edit' };
+ delivery: Delivery | null;
+ production: 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' },
+ productionDialogOptions: { open: false, mode: 'edit' },
+ delivery: null,
+ production: 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 { productions, refreshProduction } = useProduction(
+ state.selectedMaterial?.ownMaterialNumber ?? null,
+ state.selectedSite?.bpns ?? null
+ );
+ const { reportedProductions } = useReportedProduction(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 openDeliveryDialog = (d: Partial) => {
+ dispatch({ type: 'delivery', payload: d });
+ dispatch({ type: 'deliveryDialogOptions', payload: { open: true, mode: 'edit' } });
};
- const handleMaterialSelect = (material: MaterialDescriptor | null) => {
- setSelectedMaterial(material);
- setSelectedSite(null);
- setSelectedPartnerSites(null);
+ const openProductionDialog = (p: Partial, mode: 'create' | 'edit') => {
+ p.material ??= {
+ materialFlag: true,
+ productFlag: false,
+ materialNumberSupplier: state.selectedMaterial?.ownMaterialNumber ?? '',
+ materialNumberCustomer: null,
+ materialNumberCx: null,
+ name: state.selectedMaterial?.description ?? '',
+ };
+ p.measurementUnit ??= 'unit:piece';
+ dispatch({ type: 'production', payload: p });
+ dispatch({ type: 'productionDialogOptions', payload: { open: true, mode } });
};
+ const handleMaterialSelect = useCallback((material: MaterialDescriptor | null) => {
+ dispatch({ type: 'selectedMaterial', payload: material });
+ dispatch({ type: 'selectedSite', payload: null });
+ dispatch({ type: 'selectedPartnerSites', payload: null });
+ }, []);
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' ? (
+
+ ) : (
+ p.productionSiteBpns === 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: 'productionDialogOptions', payload: { open: false, mode: state.productionDialogOptions.mode } })}
+ onSave={refreshProduction}
+ production={state.production}
+ productions={state.productionDialogOptions.mode === 'edit' ? productions ?? [] : []}
+ />
>
);
};
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/DeliveryInformationModal.tsx b/frontend/src/features/dashboard/components/DeliveryInformationModal.tsx
index 6f522b67..09a624e6 100644
--- a/frontend/src/features/dashboard/components/DeliveryInformationModal.tsx
+++ b/frontend/src/features/dashboard/components/DeliveryInformationModal.tsx
@@ -51,7 +51,7 @@ export const DeliveryInformationModal = ({ open, onClose, delivery }: DeliveryIn
-
+