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 - + + + ), + }, + ] as const; + +type PlannedProductionModalProps = { + open: boolean; + mode: 'create' | 'edit'; + onClose: () => void; + onSave: () => void; + production: Partial | null; + productions: Production[]; +}; +const isValidProduction = (production: Partial) => + production && + production.estimatedTimeOfCompletion && + production.quantity && + production.measurementUnit && + production.partner && + isValidOrderReference(production); + +export const PlannedProductionModal = ({ open, mode, onClose, onSave, production, productions }: PlannedProductionModalProps) => { + const [temporaryProduction, setTemporaryProduction] = useState>(production ?? {}); + const { partners } = usePartners('product', temporaryProduction?.material?.materialNumberSupplier ?? null); + const [notifications, setNotifications] = useState([]); + const [formError, setFormError] = useState(false); + const dailyProductions = useMemo( + () => + productions.filter( + (p) => + new Date(p.estimatedTimeOfCompletion).toLocaleDateString() === + new Date(production?.estimatedTimeOfCompletion ?? Date.now()).toLocaleDateString() + ), + [productions, production?.estimatedTimeOfCompletion] + ); + + const handleSaveClick = () => { + temporaryProduction.customerOrderNumber ||= undefined; + temporaryProduction.customerOrderPositionNumber ||= undefined; + temporaryProduction.supplierOrderNumber ||= undefined; + if (!isValidProduction(temporaryProduction)) { + setFormError(true); + return; + } + setFormError(false); + postProductionRange([temporaryProduction]) + .then(() => { + onSave(); + setNotifications((ns) => [ + ...ns, + { + title: 'Production Created', + description: 'The Production 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 Production' : error.error, + severity: 'error', + }, + ]); + }) + .finally(onClose); + }; + const handleDelete = (row: Production) => { + if (row.uuid) deleteProduction(row.uuid).then(onSave); + }; + useEffect(() => { + if (production) setTemporaryProduction(production); + }, [production]); + return ( + <> + + + {capitalize(mode)} Production Information + + + + {mode === 'create' ? ( + <> + + + + + setTemporaryProduction({ ...temporaryProduction, estimatedTimeOfCompletion: date ?? undefined }) + } + /> + + + option?.name ?? ''} + renderInput={(params) => ( + + )} + onChange={(_, value) => + setTemporaryProduction({ ...temporaryProduction, partner: value ?? undefined }) + } + value={temporaryProduction.partner ?? null} + isOptionEqualToValue={(option, value) => option?.uuid === value?.uuid} + /> + + + + setTemporaryProduction((curr) => ({ + ...curr, + quantity: e.target.value ? parseFloat(e.target.value) : undefined, + })) + } + /> + + + option?.value ?? ''} + renderInput={(params) => ( + + )} + onChange={(_, value) => + setTemporaryProduction((curr) => ({ ...curr, measurementUnit: value?.key })) + } + isOptionEqualToValue={(option, value) => option?.key === value?.key} + /> + + + + setTemporaryProduction({ ...temporaryProduction, customerOrderNumber: event.target.value }) + } + /> + + + + setTemporaryProduction({ + ...temporaryProduction, + customerOrderPositionNumber: event.target.value, + }) + } + /> + + + + setTemporaryProduction({ + ...temporaryProduction, + supplierOrderNumber: event.target.value ?? '', + }) + } + /> + + + ) : ( + +
row.uuid} + columns={createProductionColumns(handleDelete)} + rows={dailyProductions} + hideFooter + density="standard" + /> + + )} + + + + {mode === 'create' && ( + + )} + + + + + {notifications.map((notification, index) => ( + setNotifications((ns) => ns.filter((_, i) => i !== index) ?? [])} + /> + ))} + + + ); +}; diff --git a/frontend/src/features/dashboard/components/ProductionTable.tsx b/frontend/src/features/dashboard/components/ProductionTable.tsx index f9b6b4e6..d3c5fe62 100644 --- a/frontend/src/features/dashboard/components/ProductionTable.tsx +++ b/frontend/src/features/dashboard/components/ProductionTable.tsx @@ -22,16 +22,33 @@ 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 { Production } from '@models/types/data/production'; +import { Add } from '@mui/icons-material'; -const createProductionRows = (numberOfDays: number, stocks: Stock[], site: Site) => { - const shipments = { - ...Object.keys(Array.from({ length: numberOfDays })).reduce((acc, _, index) => ({ ...acc, [index]: index % 3 === 1 ? 90 : 0 }), {}), +const createProductionRow = (numberOfDays: number, productions: Production[]) => { + return { + ...Object.keys(Array.from({ length: numberOfDays })).reduce((acc, _, index) => { + const date = new Date(); + date.setDate(date.getDate() + index); + const prod = productions + .filter( + (production) => new Date(production.estimatedTimeOfCompletion).toDateString() === date.toDateString() + ) + .reduce((sum, production) => sum + production.quantity, 0); + return { ...acc, [index]: prod }; + }, {}), }; - const production = { ...Object.keys(Array.from({ length: numberOfDays })).reduce((acc, _, index) => ({ ...acc, [index]: 30 }), {}) }; +}; +const createShipmentRow = (numberOfDays: number) => { + return { ...Object.keys(Array.from({ length: numberOfDays })).reduce((acc, _, index) => ({ ...acc, [index]: 0 }), {}), }; +} +const createProductionTableRows = (numberOfDays: number, stocks: Stock[], productions: Production[], site: Site) => { + const shipmentRow = createShipmentRow(numberOfDays); + const productionRow = createProductionRow(numberOfDays, productions); const stockQuantity = stocks.filter((stock) => stock.stockLocationBpns === site.bpns).reduce((acc, stock) => acc + stock.quantity, 0); - const allocatedStocks = { + const stockRow = { ...Object.keys(Array.from({ length: numberOfDays })).reduce( (acc, _, index) => ({ ...acc, @@ -39,54 +56,74 @@ const createProductionRows = (numberOfDays: number, stocks: Stock[], site: Site) index === 0 ? stockQuantity : acc[(index - 1) as keyof typeof acc] - - shipments[index as keyof typeof shipments] + - production[(index - 1) as keyof typeof production], - }), - {} - ), + shipmentRow[(index -1) as keyof typeof shipmentRow] + + productionRow[(index - 1) as keyof typeof productionRow], + }), {}), }; return [ - { id: 'shipment', name: 'Shipments', ...shipments }, - { id: 'itemStock', name: 'Item Stock', ...allocatedStocks }, - { id: 'plannedProduction', name: 'Planned Production', ...production }, + { id: 'shipment', name: 'Outgoing Shipments', ...shipmentRow }, + { id: 'itemStock', name: 'Projected Item Stock', ...stockRow }, + { id: 'plannedProduction', name: 'Planned Production', ...productionRow }, ]; }; +type ProductionTableProps = { + numberOfDays: number; + stocks: Stock[] | null; + site: Site; + productions: Production[] | null; + readOnly: boolean; + onDeliveryClick: (delivery: Delivery) => void + onProductionClick: (production: Partial, mode: 'create' | 'edit') => void; +}; -type ProductionTableProps = { numberOfDays: number; stocks: Stock[] | null; site: Site, onDeliveryClick: (delivery: Delivery) => void }; - -export const ProductionTable = ({ numberOfDays, stocks, site, onDeliveryClick }: ProductionTableProps) => { +export const ProductionTable = ({ numberOfDays, stocks, site, productions, readOnly, onDeliveryClick, onProductionClick, }: ProductionTableProps) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any - const handleDeliveryClick = (cellData: any) => { - if (cellData.id !== 'shipment') 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, - }, - }); + if (cellData.id === 'shipment') { + onDeliveryClick({ + quantity: cellData.value, + etd: cellData.colDef.headerName, + origin: { + bpns: site?.bpns, + }, + destination: { + bpns: site?.bpns, + }, + }); + } + if (cellData.id === 'plannedProduction') { + const material = stocks?.length ? stocks[0].material : undefined; + onProductionClick({ + quantity: parseFloat(cellData.value), + material, + estimatedTimeOfCompletion: new Date(cellData.colDef.headerName), + productionSiteBpns: site.bpns, + }, 'edit'); + } }; return ( - <> - - - Site:{' '} - {' '} + + + Site: {site.name} ({site.bpns}) + {!readOnly && ( + + + + )} row.id} hideFooter={true} /> - + ); }; diff --git a/frontend/src/features/dashboard/hooks/useProduction.ts b/frontend/src/features/dashboard/hooks/useProduction.ts new file mode 100644 index 00000000..faaf446b --- /dev/null +++ b/frontend/src/features/dashboard/hooks/useProduction.ts @@ -0,0 +1,33 @@ +/* +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 { Production } from '@models/types/data/production'; +import { BPNS } from '@models/types/edc/bpn'; + +export const useProduction = (materialNumber: string | null, site: BPNS | null) => { + const {data: productions, error: productionsError, isLoading: isLoadingProductions, refresh: refreshProduction } = useFetch(materialNumber && site ? `${config.app.BACKEND_BASE_URL}${config.app.ENDPOINT_PRODUCTION}?materialNumber=${materialNumber}&site=${site}` : undefined); + return { + productions, + productionsError, + isLoadingProductions, + refreshProduction, + }; +} diff --git a/frontend/src/features/dashboard/hooks/useReportedProduction.ts b/frontend/src/features/dashboard/hooks/useReportedProduction.ts new file mode 100644 index 00000000..84cbdf03 --- /dev/null +++ b/frontend/src/features/dashboard/hooks/useReportedProduction.ts @@ -0,0 +1,32 @@ +/* +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 { Production } from '@models/types/data/production'; + +export const useReportedProduction = (materialNumber: string | null) => { + const {data: reportedProductions, error: reportedProductionsError, isLoading: isLoadingReportedProductions, refresh: refreshProduction } = useFetch(materialNumber ? `${config.app.BACKEND_BASE_URL}${config.app.ENDPOINT_PRODUCTION}/reported?materialNumber=${materialNumber}` : undefined); + return { + reportedProductions, + reportedProductionsError, + isLoadingReportedProductions, + refreshProduction, + }; +} diff --git a/frontend/src/features/dashboard/util/helpers.tsx b/frontend/src/features/dashboard/util/helpers.tsx index 3b45fa14..9a4b79fb 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) => { @@ -27,12 +25,38 @@ export const createDateColumnHeaders = (numberOfDays: number) => { date.setDate(date.getDate() + index); return { field: `${index}`, - headerName: date.toLocaleDateString('en-US', { weekday: 'long', day: '2-digit', month: '2-digit', year: 'numeric' }), + headerName: date.toISOString(), + headerAlign: 'center' as const, + sortable: false, + disableColumnMenu: true, width: 180, - renderCell: (data: { value?: number } & { row: {id: number | string }}) => { + renderHeader: (data: { colDef: { headerName?: string } }) => { + return ( + + {new Date(data.colDef.headerName!).toLocaleDateString(undefined, { weekday: 'short', day: '2-digit', month: '2-digit', year: 'numeric' })} + + ); + }, + 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)'} + )} ); }, @@ -42,4 +66,3 @@ export const createDateColumnHeaders = (numberOfDays: number) => { }; export const getPartnerType = (type: 'customer' | 'supplier') => (type === 'customer' ? 'supplier' : 'customer'); - diff --git a/frontend/src/features/stock-view/components/StockDetailsView.tsx b/frontend/src/features/stock-view/components/StockDetailsView.tsx index fd75567a..d9dbbaec 100644 --- a/frontend/src/features/stock-view/components/StockDetailsView.tsx +++ b/frontend/src/features/stock-view/components/StockDetailsView.tsx @@ -31,17 +31,12 @@ import { StockTable } from './StockTable'; import { useStocks } from '../hooks/useStocks'; import { usePartnerStocks } from '../hooks/usePartnerStocks'; import { compareStocks } from '@util/stock-helpers'; +import { Notification } from '@models/types/data/notification'; type StockDetailsViewProps = { type: T; }; -type Notification = { - title: string; - description: string; - severity: 'success' | 'error'; -}; - export const StockDetailsView = ({ type }: StockDetailsViewProps) => { const { materials } = useMaterials(type); const { stocks, refreshStocks } = useStocks(type); diff --git a/frontend/src/index.css b/frontend/src/index.css index bb6f0c82..34ed5a80 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 { @@ -50,6 +50,31 @@ body { border-radius: 0; } -.table-container .MuiDataGrid-columnHeader button { - display: none; +.MuiDialog-container .MuiPaper-root { + max-width: unset !important; +} + +input[type='time'] { + box-sizing: border-box; + padding: 0.875rem; + border-radius: 4px; + border: 1px solid #c5c7cb; + +} + +input[type='time']:hover { + border-color: rgba(0, 0, 0, 0.87) +} + +input[type='time']:focus, +input[type='time']:focus-within, +input[type='time']:focus-visible, +input[type='time']:active { + outline: 1px solid #2563eb; + border-color: #2563eb; } + +input[type='time'].error { + border-color: #f44336; +} + diff --git a/frontend/src/models/constants/config.ts b/frontend/src/models/constants/config.ts index 220ef1f6..d8d986a2 100644 --- a/frontend/src/models/constants/config.ts +++ b/frontend/src/models/constants/config.ts @@ -33,6 +33,8 @@ 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_PRODUCTION: import.meta.env.VITE_ENDPOINT_PRODUCTION.trim() as string, + ENDPOINT_PRODUCTION_RANGE: import.meta.env.VITE_ENDPOINT_PRODUCTION_RANGE.trim() as string, }; const auth = { diff --git a/frontend/src/models/types/data/notification.ts b/frontend/src/models/types/data/notification.ts new file mode 100644 index 00000000..18939eb2 --- /dev/null +++ b/frontend/src/models/types/data/notification.ts @@ -0,0 +1,24 @@ +/* +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/models/types/data/order-reference.ts b/frontend/src/models/types/data/order-reference.ts new file mode 100644 index 00000000..5810b0cd --- /dev/null +++ b/frontend/src/models/types/data/order-reference.ts @@ -0,0 +1,24 @@ +/* +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 OrderReference = { + customerOrderNumber: string | null; + customerOrderPositionNumber: string | null; + supplierOrderNumber: string | null; +} diff --git a/frontend/src/models/types/data/production.ts b/frontend/src/models/types/data/production.ts new file mode 100644 index 00000000..2af94766 --- /dev/null +++ b/frontend/src/models/types/data/production.ts @@ -0,0 +1,35 @@ +/* +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 { MaterialDetails } from './stock'; +import { BPNS } from '../edc/bpn'; +import { Partner } from '../edc/partner'; +import { OrderReference } from './order-reference'; + +export type Production = { + uuid?: UUID; + partner: Partner; + material: MaterialDetails; + quantity: number; + measurementUnit: UnitOfMeasurementKey; + productionSiteBpns: BPNS; + estimatedTimeOfCompletion: Date; +} & OrderReference; diff --git a/frontend/src/models/types/data/stock.ts b/frontend/src/models/types/data/stock.ts index 731fd214..4c084d0d 100644 --- a/frontend/src/models/types/data/stock.ts +++ b/frontend/src/models/types/data/stock.ts @@ -22,8 +22,9 @@ import { UUID } from 'crypto'; import { BPNA, BPNS } from '../edc/bpn'; import { Partner } from '../edc/partner'; import { UnitOfMeasurementKey } from './uom'; +import { OrderReference } from './order-reference'; -type MaterialDetails = { +export type MaterialDetails = { uuid?: UUID | null; materialFlag: boolean; productFlag: boolean; @@ -40,12 +41,9 @@ export type Stock = { measurementUnit: UnitOfMeasurementKey; stockLocationBpns: BPNS; stockLocationBpna: BPNA; - customerOrderNumber: string | null; - customerOrderPositionNumber: string | null; - supplierOrderNumber: string | null; lastUpdatedOn: string; partner: Partner; isBlocked: boolean; -}; +} & OrderReference; export type StockType = 'material' | 'product'; diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index 5ded8be0..95b2c02a 100644 --- a/frontend/src/router.tsx +++ b/frontend/src/router.tsx @@ -26,7 +26,7 @@ import { CatalogView } from '@views/CatalogView'; import { NegotiationView } from '@views/NegotiationView'; import { TransferView } from '@views/TransferView'; import { StockView } from '@views/StockView'; -import { SupplierDashboardView } from '@views/SupplierDashboardView'; +import { DashboardView } from '@views/DashboardView'; import { AboutLicenseView } from '@views/AboutLicenseView'; import { UnauthorizedView } from '@views/errors/UnauthorizedView'; import { ErrorView } from '@views/errors/ErrorView'; @@ -46,8 +46,8 @@ export const router = createBrowserRouter([ element: , }, { - path: 'supplierDashboard', - element: , + path: 'dashboard', + element: , }, ], }, @@ -71,7 +71,7 @@ export const router = createBrowserRouter([ }, { path: '/', - loader: () => redirect('/stocks'), + loader: () => redirect('/dashboard'), }, { path: 'aboutLicense', diff --git a/frontend/src/services/productions-service.ts b/frontend/src/services/productions-service.ts new file mode 100644 index 00000000..3ae4741b --- /dev/null +++ b/frontend/src/services/productions-service.ts @@ -0,0 +1,51 @@ +/* +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 { config } from '@models/constants/config'; +import { Production } from '@models/types/data/production'; +import { UUID } from 'crypto'; + +export const postProductionRange = async (range: Partial[]) => { + const res = await fetch(config.app.BACKEND_BASE_URL + config.app.ENDPOINT_PRODUCTION_RANGE, { + method: 'POST', + body: JSON.stringify(range), + 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 deleteProduction = async (id: UUID) => { + const res = await fetch(config.app.BACKEND_BASE_URL + config.app.ENDPOINT_PRODUCTION + `/${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..e36f0933 100644 --- a/frontend/src/util/helpers.ts +++ b/frontend/src/util/helpers.ts @@ -19,6 +19,7 @@ SPDX-License-Identifier: Apache-2.0 */ import { UNITS_OF_MEASUREMENT } from '@models/constants/uom'; +import { OrderReference } from '@models/types/data/order-reference'; import { UnitOfMeasurementKey } from '@models/types/data/uom'; export const getUnitOfMeasurement = (unitOfMeasurementKey: UnitOfMeasurementKey) => @@ -37,7 +38,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); @@ -61,3 +61,7 @@ export const isErrorResponse = (response: unknown): response is ErrorResponse => typeof response[0].message === 'string' ); }; + +export const isValidOrderReference = (ref: Partial) => + (ref.customerOrderNumber && ref.customerOrderPositionNumber) || + (!ref.customerOrderNumber && !ref.customerOrderPositionNumber && !ref.supplierOrderNumber); diff --git a/frontend/src/views/SupplierDashboardView.tsx b/frontend/src/views/DashboardView.tsx similarity index 97% rename from frontend/src/views/SupplierDashboardView.tsx rename to frontend/src/views/DashboardView.tsx index eb046d6a..69622ae5 100644 --- a/frontend/src/views/SupplierDashboardView.tsx +++ b/frontend/src/views/DashboardView.tsx @@ -25,7 +25,7 @@ import { Dashboard } from '@features/dashboard/components/Dashboard'; import { Box, Stack, Typography } from '@mui/material'; import { useState } from 'react'; -export const SupplierDashboardView = () => { +export const DashboardView = () => { const [selectedTab, setSelectedTab] = useState(0); return (