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 ( + <> + + + Demand Information + + + {mode === 'create' ? ( + + + + + setTemporaryDemand((curr) => ({ ...curr, day: value }))} + /> + + + option?.value ?? ''} + onChange={(_, value) => setTemporaryDemand((curr) => ({ ...curr, demandCategoryCode: value?.key }))} + isOptionEqualToValue={(option, value) => option?.key === value?.key} + value={ + temporaryDemand.demandCategoryCode + ? { + key: temporaryDemand.demandCategoryCode, + value: DEMAND_CATEGORY.find((c) => c.key === temporaryDemand.demandCategoryCode)?.value, + } + : DEMAND_CATEGORY[0] + } + renderInput={(params) => ( + + )} + /> + + + + setTemporaryDemand((curr) => + parseFloat(e.target.value) >= 0 + ? { ...curr, quantity: parseFloat(e.target.value) } + : { ...curr, quantity: 0 } + ) + } + /> + + + option?.value ?? ''} + onChange={(_, value) => setTemporaryDemand((curr) => ({ ...curr, measurementUnit: value?.key }))} + isOptionEqualToValue={(option, value) => option?.key === value?.key} + value={ + temporaryDemand.measurementUnit + ? { + key: temporaryDemand.measurementUnit, + value: getUnitOfMeasurement(temporaryDemand.measurementUnit), + } + : null + } + renderInput={(params) => ( + + )} + /> + + + option?.name ?? ''} + isOptionEqualToValue={(option, value) => option?.uuid === value?.uuid} + value={partners?.find((s) => s.bpnl === temporaryDemand.partnerBpnl) ?? null} + onChange={(_, value) => setTemporaryDemand({ ...temporaryDemand, partnerBpnl: value?.bpnl ?? undefined })} + renderInput={(params) => ( + + )} + /> + + + s.bpnl === temporaryDemand.partnerBpnl)?.sites ?? []} + getOptionLabel={(option) => option.name ?? ''} + disabled={!temporaryDemand?.partnerBpnl} + isOptionEqualToValue={(option, value) => option?.bpns === value.bpns} + onChange={(_, value) => setTemporaryDemand({ ...temporaryDemand, supplierLocationBpns: value?.bpns ?? undefined })} + value={partners + ?.find((s) => s.bpnl === temporaryDemand.partnerBpnl) + ?.sites.find((s) => s.bpns === temporaryDemand.supplierLocationBpns) ?? null} + renderInput={(params) => ( + + )} + /> + + + ) : ( +
row.uuid} + columns={createDemandColumns(handleDelete)} + rows={dailyDemands ?? []} + hideFooter + /> + )} + + + {mode === 'create' && ( + + )} + + + + + {notifications.map((notification, index) => ( + setNotifications((ns) => ns.filter((_, i) => i !== index) ?? [])} + /> + ))} + + + ); +}; diff --git a/frontend/src/features/dashboard/components/DemandTable.tsx b/frontend/src/features/dashboard/components/DemandTable.tsx index ee75e35f..ad431fde 100644 --- a/frontend/src/features/dashboard/components/DemandTable.tsx +++ b/frontend/src/features/dashboard/components/DemandTable.tsx @@ -22,77 +22,121 @@ import { TableWithRowHeader } from '@components/TableWithRowHeader'; import { Stock } from '@models/types/data/stock'; import { Site } from '@models/types/edc/site'; import { createDateColumnHeaders } from '../util/helpers'; -import { Box, Typography } from '@mui/material'; +import { Box, Button, Stack, Typography } from '@mui/material'; import { Delivery } from '@models/types/data/delivery'; +import { Add } from '@mui/icons-material'; +import { Demand } from '@models/types/data/demand'; -const createDemandRows = (numberOfDays: number, stocks: Stock[], site: Site) => { - const demands = { ...Object.keys(Array.from({ length: numberOfDays })).reduce((acc, _, index) => ({ ...acc, [index]: 30 }), {}) }; - const deliveries = { - ...Object.keys(Array.from({ length: numberOfDays })).reduce((acc, _, index) => ({ ...acc, [index]: index % 3 === 0 ? 45 : 0 }), {}), +const createDemandRow = (numberOfDays: number, demands: Demand[]) => { + return { + ...Object.keys(Array.from({ length: numberOfDays })).reduce((acc, _, index) => { + const date = new Date(); + date.setDate(date.getDate() + index); + const demand = demands + .filter((d) => new Date(`${new Date(d.day ?? Date.now())}Z`).toDateString() === date.toDateString()) + .reduce((sum, d) => sum + d.quantity, 0); + return { ...acc, [index]: demand }; + }, {}), }; +}; + +const createDeliveryRow = (numberOfDays: number) => { + return { + ...Object.keys(Array.from({ length: numberOfDays })).reduce((acc, _, index) => ({ ...acc, [index]: 0 }), {}), + }; +} + +const createTableRows = (numberOfDays: number, stocks: Stock[], demands: Demand[], site: Site) => { + const demandRow = createDemandRow(numberOfDays, demands); + const deliveryRow = createDeliveryRow(numberOfDays); const currentStock = stocks.find((s) => s.stockLocationBpns === site.bpns)?.quantity ?? 0; const itemStock = { ...Object.keys(Array.from({ length: numberOfDays })).reduce( (acc, _, index) => ({ ...acc, [index]: - (index === 0 ? currentStock : acc[(index - 1) as keyof typeof acc]) + - deliveries[index as keyof typeof deliveries] - - demands[index as keyof typeof demands], - }), - {} - ), - }; - const daysOfSupply = { - ...Object.keys(Array.from({ length: numberOfDays })).reduce( - (acc, _, index) => ({ - ...acc, - [index]: Math.max(itemStock[index as keyof typeof itemStock] / demands[index as keyof typeof demands], 0).toFixed(2), + index === 0 ? currentStock : (acc[(index - 1) as keyof typeof acc] + + deliveryRow[(index - 1) as keyof typeof deliveryRow] - + demandRow[(index - 1) as keyof typeof demandRow]), }), {} ), }; return [ - { id: 'demand', name: 'Demand', ...demands }, - { id: 'itemStock', name: 'Item Stock', ...itemStock }, - { id: 'daysOfSupply', name: 'Days of Supply', ...daysOfSupply }, - { id: 'delivery', name: 'Delivery', ...deliveries }, + { id: 'demand', name: 'Demand', ...demandRow }, + { id: 'itemStock', name: 'Projected Item Stock', ...itemStock }, + { id: 'delivery', name: 'Incoming Deliveries', ...deliveryRow }, ]; }; -type DemandTableProps = { numberOfDays: number; stocks: Stock[] | null; site: Site; onDeliveryClick: (delivery: Delivery) => void }; +type DemandTableProps = { + numberOfDays: number; + stocks: Stock[] | null; + demands: Demand[] | null; + site: Site; + readOnly?: boolean; + onDeliveryClick: (delivery: Partial) => void; + onDemandClick: (demand: Partial, mode: 'create' | 'edit') => void; +}; -export const DemandTable = ({ numberOfDays, stocks, site, onDeliveryClick }: DemandTableProps) => { +export const DemandTable = ({ numberOfDays, stocks, demands, site, readOnly, onDeliveryClick, onDemandClick }: DemandTableProps) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any - const handleDeliveryClick = (cellData: any) => { - if (cellData.id !== 'delivery') return; + const handleCellClick = (cellData: any) => { if (cellData.value === 0) return; - onDeliveryClick({ - quantity: cellData.value, - etd: cellData.colDef.headerName, - origin: { - bpns: site?.bpns, - }, - destination: { - bpns: site?.bpns, - }, - }); + switch (cellData.id) { + case 'delivery': + onDeliveryClick({ + quantity: cellData.value, + etd: cellData.colDef.headerName, + origin: { + bpns: site?.bpns, + }, + destination: { + bpns: site?.bpns, + }, + }); + break; + case 'demand': + onDemandClick({ + quantity: parseFloat(cellData.value), + ownMaterialNumber: stocks ? stocks[0].material?.materialNumberCustomer : undefined, + demandLocationBpns: site.bpns, + day: new Date(cellData.colDef.headerName), + }, 'edit'); + break; + default: + break; + } }; return ( - <> + - Site: + + Site: + {site.name} ({site.bpns}) + {!readOnly && } row.id} hideFooter={true} /> - + ); }; diff --git a/frontend/src/features/dashboard/hooks/useDemand.ts b/frontend/src/features/dashboard/hooks/useDemand.ts new file mode 100644 index 00000000..fe3c967f --- /dev/null +++ b/frontend/src/features/dashboard/hooks/useDemand.ts @@ -0,0 +1,34 @@ +/* +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 { useFetch } from '@hooks/useFetch' +import { config } from '@models/constants/config' +import { Demand } from '@models/types/data/demand'; +import { BPNS } from '@models/types/edc/bpn'; + +export const useDemand = (materialNumber: string | null, site: BPNS | null) => { + const {data: demands, error: demandsError, isLoading: isLoadingDemands, refresh: refreshDemand } = useFetch(materialNumber && site ? `${config.app.BACKEND_BASE_URL}${config.app.ENDPOINT_DEMAND}?materialNumber=${materialNumber}&site=${site}` : undefined); + return { + demands, + demandsError, + isLoadingDemands, + refreshDemand, + }; +} \ No newline at end of file diff --git a/frontend/src/features/dashboard/hooks/useReportedDemand.ts b/frontend/src/features/dashboard/hooks/useReportedDemand.ts new file mode 100644 index 00000000..81753f01 --- /dev/null +++ b/frontend/src/features/dashboard/hooks/useReportedDemand.ts @@ -0,0 +1,13 @@ +import { useFetch } from '@hooks/useFetch'; +import { config } from '@models/constants/config'; +import { Demand } from '@models/types/data/demand'; + +export const useReportedDemand = (materialNumber: string | null) => { + const {data: reportedDemands, error: reportedDemandsError, isLoading: isLoadingReportedDemands, refresh: refreshDemand } = useFetch(materialNumber ? `${config.app.BACKEND_BASE_URL}${config.app.ENDPOINT_DEMAND}/reported?materialNumber=${materialNumber}` : undefined); + return { + reportedDemands, + reportedDemandsError, + isLoadingReportedDemands, + refreshDemand, + }; +} diff --git a/frontend/src/features/dashboard/util/helpers.tsx b/frontend/src/features/dashboard/util/helpers.tsx index 3b45fa14..7af40667 100644 --- a/frontend/src/features/dashboard/util/helpers.tsx +++ b/frontend/src/features/dashboard/util/helpers.tsx @@ -17,8 +17,6 @@ under the License. SPDX-License-Identifier: Apache-2.0 */ - -import { Info } from '@mui/icons-material'; import { Box, Button } from '@mui/material'; export const createDateColumnHeaders = (numberOfDays: number) => { @@ -28,11 +26,30 @@ export const createDateColumnHeaders = (numberOfDays: number) => { return { field: `${index}`, headerName: date.toLocaleDateString('en-US', { weekday: 'long', day: '2-digit', month: '2-digit', year: 'numeric' }), + headerAlign: 'center' as const, + sortable: false, + disableColumnMenu: true, width: 180, - renderCell: (data: { value?: number } & { row: {id: number | string }}) => { + renderCell: (data: { value?: number, field: string } & { row: { id: number | string } }) => { return ( - - {(data.row.id === 'delivery' || data.row.id === 'shipment') && data.value !== 0 ? : data.value} + + {(data.row.id === 'delivery' || data.row.id === 'shipment' || data.row.id === 'plannedProduction' || data.row.id === 'demand') && + data.value !== 0 ? ( + + ) : (<> + {(data.value ?? 0) > 0 ? data.value : 0} + {data.row.id === 'itemStock' && data.field === '0' && '(current)'} + )} ); }, diff --git a/frontend/src/hooks/useDemand.ts b/frontend/src/hooks/useDemand.ts new file mode 100644 index 00000000..fe3c967f --- /dev/null +++ b/frontend/src/hooks/useDemand.ts @@ -0,0 +1,34 @@ +/* +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 { useFetch } from '@hooks/useFetch' +import { config } from '@models/constants/config' +import { Demand } from '@models/types/data/demand'; +import { BPNS } from '@models/types/edc/bpn'; + +export const useDemand = (materialNumber: string | null, site: BPNS | null) => { + const {data: demands, error: demandsError, isLoading: isLoadingDemands, refresh: refreshDemand } = useFetch(materialNumber && site ? `${config.app.BACKEND_BASE_URL}${config.app.ENDPOINT_DEMAND}?materialNumber=${materialNumber}&site=${site}` : undefined); + return { + demands, + demandsError, + isLoadingDemands, + refreshDemand, + }; +} \ No newline at end of file diff --git a/frontend/src/index.css b/frontend/src/index.css index bb6f0c82..5d0231e5 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -28,7 +28,7 @@ body { } [role='tabpanel'] { - @apply w-full; + width: 100%; } .MuiDataGrid-root > .MuiBox-root h5 > span { @@ -42,7 +42,7 @@ body { } .table-container .MuiDataGrid-root > .MuiBox-root { - display: none; + height: 0; } .table-container .MuiDataGrid-root { @@ -53,3 +53,7 @@ body { .table-container .MuiDataGrid-columnHeader button { display: none; } + +.MuiDialog-container .MuiPaper-root { + max-width: unset !important; +} diff --git a/frontend/src/models/constants/config.ts b/frontend/src/models/constants/config.ts index 220ef1f6..88fcfb16 100644 --- a/frontend/src/models/constants/config.ts +++ b/frontend/src/models/constants/config.ts @@ -33,6 +33,7 @@ const app = { ENDPOINT_UPDATE_REPORTED_MATERIAL_STOCKS: import.meta.env.VITE_ENDPOINT_UPDATE_REPORTED_MATERIAL_STOCKS.trim() as string, ENDPOINT_UPDATE_REPORTED_PRODUCT_STOCKS: import.meta.env.VITE_ENDPOINT_UPDATE_REPORTED_PRODUCT_STOCKS.trim() as string, ENDPOINT_PARTNER_OWNSITES: import.meta.env.VITE_ENDPOINT_PARTNER_OWNSITES.trim() as string, + ENDPOINT_DEMAND: import.meta.env.VITE_ENDPOINT_DEMAND.trim() as string, }; const auth = { diff --git a/frontend/src/models/constants/demand-category.ts b/frontend/src/models/constants/demand-category.ts new file mode 100644 index 00000000..ba715acd --- /dev/null +++ b/frontend/src/models/constants/demand-category.ts @@ -0,0 +1,54 @@ +/* +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 +*/ + +export const DEMAND_CATEGORY = [ + { + "key": "0001", + "value": "Default" + }, + { + "key": "A1S1", + "value": "After-Sales" + }, + { + "key": "SR99", + "value": "Series" + }, + { + "key": "PI01", + "value": "Phase-In-Period" + }, + { + "key": "OS01", + "value": "Single-Order" + }, + { + "key": "OI01", + "value": "Small Series" + }, + { + "key": "ED01", + "value": "Extraordinary Demand" + }, + { + "key": "PO01", + "value": "Phase-Out-Period" + }, +]; diff --git a/frontend/src/models/types/data/demand-category.ts b/frontend/src/models/types/data/demand-category.ts new file mode 100644 index 00000000..073bb6df --- /dev/null +++ b/frontend/src/models/types/data/demand-category.ts @@ -0,0 +1,27 @@ +/* +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 { DEMAND_CATEGORY } from '../../constants/demand-category'; + +export type DemandCategoryCode = typeof DEMAND_CATEGORY[number]['key']; + +export type DemandCategory = { + key: DemandCategoryCode; + value: string; +} diff --git a/frontend/src/models/types/data/demand.ts b/frontend/src/models/types/data/demand.ts new file mode 100644 index 00000000..e2bb2fc0 --- /dev/null +++ b/frontend/src/models/types/data/demand.ts @@ -0,0 +1,36 @@ +/* +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 { UUID } from 'crypto'; +import { UnitOfMeasurementKey } from './uom'; +import { BPNS } from '../edc/bpn'; +import { DemandCategoryCode } from './demand-category'; + +export type Demand = { + uuid?: UUID; + ownMaterialNumber: string | null; + demandLocationBpns: BPNS; + partnerBpnl: string; + supplierLocationBpns: string; + quantity: number; + measurementUnit: UnitOfMeasurementKey; + day: Date | null; + demandCategoryCode: DemandCategoryCode; +} diff --git a/frontend/src/models/types/data/notification.ts b/frontend/src/models/types/data/notification.ts new file mode 100644 index 00000000..7b89d77e --- /dev/null +++ b/frontend/src/models/types/data/notification.ts @@ -0,0 +1,25 @@ +/* +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 +*/ + +export type Notification = { + title: string; + description: string; + severity: 'success' | 'error'; +}; diff --git a/frontend/src/services/demands-service.ts b/frontend/src/services/demands-service.ts new file mode 100644 index 00000000..3a2d53cb --- /dev/null +++ b/frontend/src/services/demands-service.ts @@ -0,0 +1,52 @@ +/* +Copyright (c) 2023,2024 Volkswagen AG +Copyright (c) 2023,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 { config } from '@models/constants/config'; +import { Demand } from '@models/types/data/demand'; +import { UUID } from 'crypto'; + +export const postDemand = async (demand: Partial) => { + const res = await fetch(config.app.BACKEND_BASE_URL + config.app.ENDPOINT_DEMAND, { + method: 'POST', + body: JSON.stringify(demand), + headers: { + 'Content-Type': 'application/json', + 'X-API-KEY': config.app.BACKEND_API_KEY, + }, + }); + if(res.status >= 400) { + const error = await res.json(); + throw error; + } + return res.json(); +} + +export const deleteDemand = async (id: UUID) => { + const res = await fetch(config.app.BACKEND_BASE_URL + config.app.ENDPOINT_DEMAND + `/${id}`, { + method: 'DELETE', + headers: { + 'X-API-KEY': config.app.BACKEND_API_KEY, + }, + }); + if(res.status >= 400) { + const error = await res.json(); + throw error; + } +} diff --git a/frontend/src/util/helpers.ts b/frontend/src/util/helpers.ts index 31f537ad..ab738c3f 100644 --- a/frontend/src/util/helpers.ts +++ b/frontend/src/util/helpers.ts @@ -37,7 +37,6 @@ export const getCatalogOperator = (operatorId: string) => { /*** * Type predicate to check if a value is an array - * * Unlike Array.isArray, this predicate asserts the members of the array to be unknown rather than any */ export const isArray = (value: unknown): value is unknown[] => Array.isArray(value); From c366397768d333418bc18a219ee8983639096abd Mon Sep 17 00:00:00 2001 From: ReneSchroederLJ Date: Fri, 19 Apr 2024 08:43:02 +0200 Subject: [PATCH 2/3] fix: demand constraints and datae bug --- .../logic/services/OwnDemandService.java | 3 +- .../logic/services/ReportedDemandService.java | 3 +- frontend/.env.dockerbuild | 2 + .../dashboard/components/Dashboard.tsx | 10 +- .../components/DemandCategoryModal.tsx | 193 +++++++++--------- .../dashboard/components/DemandTable.tsx | 4 +- frontend/src/models/types/data/demand.ts | 6 +- local/docker-compose.yaml | 6 + 8 files changed, 116 insertions(+), 111 deletions(-) diff --git a/backend/src/main/java/org/eclipse/tractusx/puris/backend/demand/logic/services/OwnDemandService.java b/backend/src/main/java/org/eclipse/tractusx/puris/backend/demand/logic/services/OwnDemandService.java index 333771b3..68ed3bf2 100644 --- a/backend/src/main/java/org/eclipse/tractusx/puris/backend/demand/logic/services/OwnDemandService.java +++ b/backend/src/main/java/org/eclipse/tractusx/puris/backend/demand/logic/services/OwnDemandService.java @@ -44,9 +44,8 @@ public boolean validate(OwnDemand demand) { demand.getDay() != null && demand.getDemandCategoryCode() != null && demand.getDemandLocationBpns() != null && - demand.getSupplierLocationBpns() != null && !demand.getPartner().equals(ownPartnerEntity) && ownPartnerEntity.getSites().stream().anyMatch(site -> site.getBpns().equals(demand.getDemandLocationBpns())) && - demand.getPartner().getSites().stream().anyMatch(site -> site.getBpns().equals(demand.getSupplierLocationBpns())); + (demand.getSupplierLocationBpns() == null || demand.getPartner().getSites().stream().anyMatch(site -> site.getBpns().equals(demand.getSupplierLocationBpns()))); } } diff --git a/backend/src/main/java/org/eclipse/tractusx/puris/backend/demand/logic/services/ReportedDemandService.java b/backend/src/main/java/org/eclipse/tractusx/puris/backend/demand/logic/services/ReportedDemandService.java index 4bad121e..ec55221e 100644 --- a/backend/src/main/java/org/eclipse/tractusx/puris/backend/demand/logic/services/ReportedDemandService.java +++ b/backend/src/main/java/org/eclipse/tractusx/puris/backend/demand/logic/services/ReportedDemandService.java @@ -45,9 +45,8 @@ public boolean validate(ReportedDemand demand) { demand.getDay() != null && demand.getDemandCategoryCode() != null && demand.getDemandLocationBpns() != null && - demand.getSupplierLocationBpns() != null && !demand.getPartner().equals(ownPartnerEntity) && - ownPartnerEntity.getSites().stream().anyMatch(site -> site.getBpns().equals(demand.getSupplierLocationBpns())) && + (demand.getSupplierLocationBpns() == null || ownPartnerEntity.getSites().stream().anyMatch(site -> site.getBpns().equals(demand.getSupplierLocationBpns()))) && demand.getPartner().getSites().stream().anyMatch(site -> site.getBpns().equals(demand.getDemandLocationBpns())); } } diff --git a/frontend/.env.dockerbuild b/frontend/.env.dockerbuild index 07d62fe9..77bcbb7d 100644 --- a/frontend/.env.dockerbuild +++ b/frontend/.env.dockerbuild @@ -13,6 +13,8 @@ VITE_ENDPOINT_UPDATE_REPORTED_MATERIAL_STOCKS=\$ENDPOINT_UPDATE_REPORTED_MATERIA VITE_ENDPOINT_UPDATE_REPORTED_PRODUCT_STOCKS=\$ENDPOINT_UPDATE_REPORTED_PRODUCT_STOCKS VITE_ENDPOINT_PARTNER_OWNSITES=\$ENDPOINT_PARTNER_OWNSITES VITE_ENDPOINT_DEMAND=\$ENDPOINT_DEMAND +VITE_ENDPOINT_PRODUCTION=\$ENDPOINT_PRODUCTION +VITE_ENDPOINT_PRODUCTION_RANGE=\$ENDPOINT_PRODUCTION_RANGE VITE_IDP_DISABLE=\$IDP_DISABLE VITE_IDP_URL=\$IDP_URL diff --git a/frontend/src/features/dashboard/components/Dashboard.tsx b/frontend/src/features/dashboard/components/Dashboard.tsx index d550e24c..f4969e3d 100644 --- a/frontend/src/features/dashboard/components/Dashboard.tsx +++ b/frontend/src/features/dashboard/components/Dashboard.tsx @@ -105,16 +105,13 @@ export const Dashboard = ({ type }: { type: 'customer' | 'supplier' }) => { 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; + d.ownMaterialNumber = state.selectedMaterial?.ownMaterialNumber ?? ''; dispatch({ type: 'demand', payload: d }); dispatch({ type: 'demandDialogOptions', payload: { open: true, mode } }); + } const openProductionDialog = (p: Partial, mode: 'create' | 'edit') => { p.material ??= { materialFlag: true, @@ -253,6 +250,7 @@ export const Dashboard = ({ type }: { type: 'customer' | 'supplier' }) => { onSave={refreshDemand} demand={state.demand} demands={demands ?? []} + /> dispatch({ type: 'productionDialogOptions', payload: { open: false, mode: state.productionDialogOptions.mode } })} @@ -262,4 +260,4 @@ export const Dashboard = ({ type }: { type: 'customer' | 'supplier' }) => { /> ); -}; +} diff --git a/frontend/src/features/dashboard/components/DemandCategoryModal.tsx b/frontend/src/features/dashboard/components/DemandCategoryModal.tsx index a0b299ff..cf792b61 100644 --- a/frontend/src/features/dashboard/components/DemandCategoryModal.tsx +++ b/frontend/src/features/dashboard/components/DemandCategoryModal.tsx @@ -42,90 +42,91 @@ const GridItem = ({ label, value }: { label: string; value: string }) => ( ); -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)}`} - - ); +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: '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: '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: '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 ( - - - - ); + { + field: 'delete', + headerName: '', + sortable: false, + disableColumnMenu: true, + headerAlign: 'center', + type: 'string', + width: 30, + renderCell: (data: { row: Demand }) => { + return ( + + + + ); + }, }, - }, -] as const; + ] as const; type DemandCategoryModalProps = { open: boolean; @@ -141,8 +142,7 @@ const isValidDemand = (demand: Partial) => demand?.quantity && demand.demandCategoryCode && demand?.measurementUnit && - demand?.partnerBpnl && - demand?.supplierLocationBpns; + demand?.partnerBpnl; export const DemandCategoryModal = ({ open, mode, onClose, onSave, demand, demands }: DemandCategoryModalProps) => { const [temporaryDemand, setTemporaryDemand] = useState>(demand ?? {}); @@ -152,9 +152,7 @@ export const DemandCategoryModal = ({ open, mode, onClose, onSave, demand, deman const dailyDemands = useMemo( () => demands?.filter( - (d) => - d.day && (new Date(d.day).toLocaleDateString() === - new Date(demand?.day ?? Date.now()).toLocaleDateString()) + (d) => d.day && new Date(d.day).toLocaleDateString() === new Date(demand?.day ?? Date.now()).toLocaleDateString() ), [demands, demand?.day] ); @@ -219,7 +217,7 @@ export const DemandCategoryModal = ({ open, mode, onClose, onSave, demand, deman error={formError && !temporaryDemand?.day} readOnly={false} value={temporaryDemand?.day} - onChangeItem={(value) => setTemporaryDemand((curr) => ({ ...curr, day: value }))} + onChangeItem={(value) => setTemporaryDemand((curr) => ({ ...curr, day: value ?? undefined }))} /> @@ -252,7 +250,7 @@ export const DemandCategoryModal = ({ open, mode, onClose, onSave, demand, deman @@ -297,7 +295,9 @@ export const DemandCategoryModal = ({ open, mode, onClose, onSave, demand, deman getOptionLabel={(option) => option?.name ?? ''} isOptionEqualToValue={(option, value) => option?.uuid === value?.uuid} value={partners?.find((s) => s.bpnl === temporaryDemand.partnerBpnl) ?? null} - onChange={(_, value) => setTemporaryDemand({ ...temporaryDemand, partnerBpnl: value?.bpnl ?? undefined })} + onChange={(_, value) => + setTemporaryDemand({ ...temporaryDemand, partnerBpnl: value?.bpnl ?? undefined }) + } renderInput={(params) => ( option.name ?? ''} disabled={!temporaryDemand?.partnerBpnl} isOptionEqualToValue={(option, value) => option?.bpns === value.bpns} - onChange={(_, value) => setTemporaryDemand({ ...temporaryDemand, supplierLocationBpns: value?.bpns ?? undefined })} - value={partners - ?.find((s) => s.bpnl === temporaryDemand.partnerBpnl) - ?.sites.find((s) => s.bpns === temporaryDemand.supplierLocationBpns) ?? null} + onChange={(_, value) => + setTemporaryDemand({ ...temporaryDemand, supplierLocationBpns: value?.bpns ?? undefined }) + } + value={ + partners + ?.find((s) => s.bpnl === temporaryDemand.partnerBpnl) + ?.sites.find((s) => s.bpns === temporaryDemand.supplierLocationBpns) ?? null + } renderInput={(params) => ( )} /> @@ -335,7 +338,7 @@ export const DemandCategoryModal = ({ open, mode, onClose, onSave, demand, deman title={`Material Demand ${ temporaryDemand?.day ? ' on ' + - new Date(temporaryDemand?.day).toLocaleDateString('en-UK', { + new Date(temporaryDemand?.day).toLocaleDateString(undefined, { weekday: 'long', day: '2-digit', month: '2-digit', @@ -343,7 +346,7 @@ export const DemandCategoryModal = ({ open, mode, onClose, onSave, demand, deman }) : '' }`} - density='standard' + density="standard" getRowId={(row) => row.uuid} columns={createDemandColumns(handleDelete)} rows={dailyDemands ?? []} diff --git a/frontend/src/features/dashboard/components/DemandTable.tsx b/frontend/src/features/dashboard/components/DemandTable.tsx index ad431fde..a2164d08 100644 --- a/frontend/src/features/dashboard/components/DemandTable.tsx +++ b/frontend/src/features/dashboard/components/DemandTable.tsx @@ -33,7 +33,7 @@ const createDemandRow = (numberOfDays: number, demands: Demand[]) => { const date = new Date(); date.setDate(date.getDate() + index); const demand = demands - .filter((d) => new Date(`${new Date(d.day ?? Date.now())}Z`).toDateString() === date.toDateString()) + .filter((d) => new Date(d.day).toDateString() === date.toDateString()) .reduce((sum, d) => sum + d.quantity, 0); return { ...acc, [index]: demand }; }, {}), @@ -99,7 +99,6 @@ export const DemandTable = ({ numberOfDays, stocks, demands, site, readOnly, onD case 'demand': onDemandClick({ quantity: parseFloat(cellData.value), - ownMaterialNumber: stocks ? stocks[0].material?.materialNumberCustomer : undefined, demandLocationBpns: site.bpns, day: new Date(cellData.colDef.headerName), }, 'edit'); @@ -120,7 +119,6 @@ export const DemandTable = ({ numberOfDays, stocks, demands, site, readOnly, onD onClick={() => onDemandClick({ demandLocationBpns: site.bpns, - ownMaterialNumber: stocks?.length ? stocks[0].material.materialNumberCustomer : null, }, 'create') } sx={{ marginLeft: 'auto' }} diff --git a/frontend/src/models/types/data/demand.ts b/frontend/src/models/types/data/demand.ts index e2bb2fc0..bb95e01f 100644 --- a/frontend/src/models/types/data/demand.ts +++ b/frontend/src/models/types/data/demand.ts @@ -25,12 +25,12 @@ import { DemandCategoryCode } from './demand-category'; export type Demand = { uuid?: UUID; - ownMaterialNumber: string | null; + ownMaterialNumber: string; demandLocationBpns: BPNS; partnerBpnl: string; - supplierLocationBpns: string; + supplierLocationBpns?: string; quantity: number; measurementUnit: UnitOfMeasurementKey; - day: Date | null; + day: Date; demandCategoryCode: DemandCategoryCode; } diff --git a/local/docker-compose.yaml b/local/docker-compose.yaml index ceb67859..b0accbcd 100644 --- a/local/docker-compose.yaml +++ b/local/docker-compose.yaml @@ -41,6 +41,9 @@ services: - ENDPOINT_UPDATE_REPORTED_MATERIAL_STOCKS=stockView/update-reported-material-stocks?ownMaterialNumber= - ENDPOINT_UPDATE_REPORTED_PRODUCT_STOCKS=stockView/update-reported-product-stocks?ownMaterialNumber= - ENDPOINT_PARTNER_OWNSITES=partners/ownSites + - ENDPOINT_DEMAND=demand + - ENDPOINT_PRODUCTION=production + - ENDPOINT_PRODUCTION_RANGE=production/range - IDP_DISABLE=true - NGINX_RATE_LIMIT=10m - NGINX_BURST=30 @@ -182,6 +185,9 @@ services: - ENDPOINT_UPDATE_REPORTED_MATERIAL_STOCKS=stockView/update-reported-material-stocks?ownMaterialNumber= - ENDPOINT_UPDATE_REPORTED_PRODUCT_STOCKS=stockView/update-reported-product-stocks?ownMaterialNumber= - ENDPOINT_PARTNER_OWNSITES=partners/ownSites + - ENDPOINT_DEMAND=demand + - ENDPOINT_PRODUCTION=production + - ENDPOINT_PRODUCTION_RANGE=production/range - IDP_DISABLE=true - NGINX_RATE_LIMIT=10m - NGINX_BURST=30 From d6f66d33a22865edeac6fe4be886d9f47d4374aa Mon Sep 17 00:00:00 2001 From: ReneSchroederLJ Date: Fri, 19 Apr 2024 13:30:06 +0200 Subject: [PATCH 3/3] chore: fixed missing semicolon --- frontend/src/features/dashboard/components/Dashboard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/features/dashboard/components/Dashboard.tsx b/frontend/src/features/dashboard/components/Dashboard.tsx index f4969e3d..d46b0cd4 100644 --- a/frontend/src/features/dashboard/components/Dashboard.tsx +++ b/frontend/src/features/dashboard/components/Dashboard.tsx @@ -111,7 +111,7 @@ export const Dashboard = ({ type }: { type: 'customer' | 'supplier' }) => { d.ownMaterialNumber = state.selectedMaterial?.ownMaterialNumber ?? ''; dispatch({ type: 'demand', payload: d }); dispatch({ type: 'demandDialogOptions', payload: { open: true, mode } }); - } + }; const openProductionDialog = (p: Partial, mode: 'create' | 'edit') => { p.material ??= { materialFlag: true,