diff --git a/backend/src/main/java/org/eclipse/tractusx/puris/backend/delivery/logic/service/OwnDeliveryService.java b/backend/src/main/java/org/eclipse/tractusx/puris/backend/delivery/logic/service/OwnDeliveryService.java index 16683d89..2ccca9d0 100644 --- a/backend/src/main/java/org/eclipse/tractusx/puris/backend/delivery/logic/service/OwnDeliveryService.java +++ b/backend/src/main/java/org/eclipse/tractusx/puris/backend/delivery/logic/service/OwnDeliveryService.java @@ -125,9 +125,8 @@ public boolean validate(OwnDelivery delivery) { delivery.getMeasurementUnit() != null && delivery.getMaterial() != null && delivery.getPartner() != null && - delivery.getTrackingNumber() != null && validateResponsibility(delivery) && - this.validateTransitEvent(delivery) && + validateTransitEvent(delivery) && !delivery.getPartner().equals(ownPartnerEntity) && (( delivery.getCustomerOrderNumber() != null && diff --git a/backend/src/main/java/org/eclipse/tractusx/puris/backend/delivery/logic/service/ReportedDeliveryService.java b/backend/src/main/java/org/eclipse/tractusx/puris/backend/delivery/logic/service/ReportedDeliveryService.java index e6a74c73..3ff3c60b 100644 --- a/backend/src/main/java/org/eclipse/tractusx/puris/backend/delivery/logic/service/ReportedDeliveryService.java +++ b/backend/src/main/java/org/eclipse/tractusx/puris/backend/delivery/logic/service/ReportedDeliveryService.java @@ -115,9 +115,8 @@ public boolean validate(ReportedDelivery delivery) { delivery.getMeasurementUnit() != null && delivery.getMaterial() != null && delivery.getPartner() != null && - delivery.getTrackingNumber() != null && validateResponsibility(delivery) && - this.validateTransitEvent(delivery) && + validateTransitEvent(delivery) && (( delivery.getCustomerOrderNumber() != null && delivery.getCustomerOrderPositionNumber() != null @@ -148,12 +147,12 @@ private boolean validateResponsibility(ReportedDelivery delivery) { return delivery.getIncoterm() != null && switch (delivery.getIncoterm().getResponsibility()) { case CUSTOMER -> delivery.getMaterial().isProductFlag() && - ownPartnerEntity.getSites().stream().anyMatch(site -> site.getBpns().equals(delivery.getDestinationBpns())) && - delivery.getPartner().getSites().stream().anyMatch(site -> site.getBpns().equals(delivery.getOriginBpns())); + ownPartnerEntity.getSites().stream().anyMatch(site -> site.getBpns().equals(delivery.getOriginBpns())) && + delivery.getPartner().getSites().stream().anyMatch(site -> site.getBpns().equals(delivery.getDestinationBpns())); case SUPPLIER -> delivery.getMaterial().isMaterialFlag() && - delivery.getPartner().getSites().stream().anyMatch(site -> site.getBpns().equals(delivery.getDestinationBpns())) && - ownPartnerEntity.getSites().stream().anyMatch(site -> site.getBpns().equals(delivery.getOriginBpns())); + delivery.getPartner().getSites().stream().anyMatch(site -> site.getBpns().equals(delivery.getOriginBpns())) && + ownPartnerEntity.getSites().stream().anyMatch(site -> site.getBpns().equals(delivery.getDestinationBpns())); case PARTIAL -> ( delivery.getMaterial().isMaterialFlag() && diff --git a/charts/puris/Chart.yaml b/charts/puris/Chart.yaml index a6ada138..06142577 100644 --- a/charts/puris/Chart.yaml +++ b/charts/puris/Chart.yaml @@ -35,7 +35,7 @@ dependencies: # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 2.2.0 +version: 2.3.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to diff --git a/charts/puris/README.md b/charts/puris/README.md index 1f6bb1d9..31c5e44a 100644 --- a/charts/puris/README.md +++ b/charts/puris/README.md @@ -1,6 +1,6 @@ # puris -![Version: 2.0.1](https://img.shields.io/badge/Version-2.0.1-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.0.0](https://img.shields.io/badge/AppVersion-1.0.0-informational?style=flat-square) +![Version: 2.3.0](https://img.shields.io/badge/Version-2.3.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.0.0](https://img.shields.io/badge/AppVersion-1.0.0-informational?style=flat-square) A helm chart for Kubernetes deployment of PURIS @@ -64,6 +64,8 @@ $ helm install puris --namespace puris --create-namespace . | backend.puris.datasource.password | string | `nil` | Password for the database user. Ignored if postgres.enabled is true. | | backend.puris.datasource.url | string | `"jdbc:postgresql://postgresql-name:5432/puris-database"` | URL of the database. Ignored if postgres.enabled is true. | | backend.puris.datasource.username | string | `"db-user"` | Username of the database. Ignored if postgres.enabled is true. | +| backend.puris.deliverysubmodel.apiassetid | string | `"deliverysubmodel-api-asset"` | Asset ID for DeliverySubmodel API | +| backend.puris.demandsubmodel.apiassetid | string | `"demandsubmodel-api-asset"` | Asset ID for DemandSubmodel API | | backend.puris.demonstrator.role | string | `nil` | Current role of the PURIS demonstrator. Default value should be empty. Can be set to "customer" or "supplier" to enable demonstration setup | | backend.puris.dtr.url | string | `"http://localhost:4243"` | Endpoint for DTR | | backend.puris.edc.controlplane.host | string | `"172.17.0.2"` | | @@ -86,6 +88,7 @@ $ helm install puris --namespace puris --create-namespace . | backend.puris.own.site.name | string | `"YOUR-SITE-NAME"` | Own site name | | backend.puris.own.streetnumber | string | `"Musterstraße 110A"` | Own street and number | | backend.puris.own.zipcodeandcity | string | `"12345 Musterhausen"` | Own zipcode and city | +| backend.puris.productionsubmodel.apiassetid | string | `"productionsubmodel-api-asset"` | Asset ID for ProductionSubmodel API | | backend.readinessProbe | object | `{"failureThreshold":3,"initialDelaySeconds":120,"periodSeconds":25,"successThreshold":1,"timeoutSeconds":1}` | Checks if the pod is fully ready to operate | | backend.readinessProbe.failureThreshold | int | `3` | Number of failures (threshold) for a readiness probe | | backend.readinessProbe.initialDelaySeconds | int | `120` | Delay in seconds after which an initial readiness probe is checked | @@ -137,10 +140,14 @@ $ helm install puris --namespace puris --create-namespace . | frontend.puris.appName | string | `"PURIS"` | The name of the app displayed in the frontend | | frontend.puris.baseUrl | string | `"your-backend-host-address.com"` | The base URL for the backend base URL without further endpoints | | frontend.puris.endpointCustomer | string | `"stockView/customer?ownMaterialNumber="` | The endpoint for the customers who buy a material identified via the own material number for the stock view | +| frontend.puris.endpointDelivery | string | `"delivery"` | The endpoint for the delivery submodel | +| frontend.puris.endpointDemand | string | `"demand"` | The endpoint for the demand submodel | | frontend.puris.endpointMaterialStocks | string | `"stockView/material-stocks"` | The endpoint for material stocks for the stock view | | frontend.puris.endpointMaterials | string | `"stockView/materials"` | The endpoint for materials for the stock view | | frontend.puris.endpointPartnerOwnSites | string | `"partners/ownSites"` | The endpoint for the partners BPNS | | frontend.puris.endpointProductStocks | string | `"stockView/product-stocks"` | The endpoint for product stocks for the stock view | +| frontend.puris.endpointProduction | string | `"production"` | The endpoint for the production submodel | +| frontend.puris.endpointProductionRange | string | `"production/range"` | The endpoint for the production range of the production submodel | | frontend.puris.endpointProducts | string | `"stockView/products"` | The endpoint for products for the stock view | | frontend.puris.endpointReportedMaterialStocks | string | `"stockView/reported-material-stocks?ownMaterialNumber="` | The endpoint for the partners' (supplier) material stocks that they potentially will deliver to me | | frontend.puris.endpointReportedProductStocks | string | `"stockView/reported-product-stocks?ownMaterialNumber="` | The endpoint for the partners' (customer) product stocks that they received from me | @@ -184,4 +191,3 @@ $ helm install puris --namespace puris --create-namespace . | postgresql.enabled | bool | `true` | Enable postgres by default, set to false to use existing postgres. Make sure to set backend.puris.jpa.hibernate.ddl-auto accordingly (by default database is created using hibernate ddl from backend). | | postgresql.fullnameOverride | string | `"backend-postgresql"` | Possibility to override the fullname | | postgresql.service.ports.postgresql | int | `5432` | Port of postgres database. | - diff --git a/charts/puris/templates/frontend-deployment.yaml b/charts/puris/templates/frontend-deployment.yaml index 98463ffc..af53a91a 100644 --- a/charts/puris/templates/frontend-deployment.yaml +++ b/charts/puris/templates/frontend-deployment.yaml @@ -80,7 +80,15 @@ spec: - name: ENDPOINT_UPDATE_REPORTED_PRODUCT_STOCKS value: "{{ .Values.frontend.puris.endpointUpdateReportedProductStocks }}" - name: ENDPOINT_PARTNER_OWNSITES - value: "{{ .Values.frontend.puris.endpointPartnerOwnSites }}" + value: "{{ .Values.frontend.puris.endpointPartnerOwnSites }}" + - name: ENDPOINT_DEMAND + value: "{{ .Values.frontend.puris.endpointDemand }}" + - name: ENDPOINT_PRODUCTION + value: "{{ .Values.frontend.puris.endpointProduction }}" + - name: ENDPOINT_PRODUCTION_RANGE + value: "{{ .Values.frontend.puris.endpointProductionRange }}" + - name: ENDPOINT_DELIVERY + value: "{{ .Values.frontend.puris.endpointDelivery }}" - name: BACKEND_API_KEY value: "{{ .Values.backend.puris.api.key}}" - name: IDP_DISABLE diff --git a/charts/puris/values.yaml b/charts/puris/values.yaml index 0785491f..c49c3de4 100644 --- a/charts/puris/values.yaml +++ b/charts/puris/values.yaml @@ -187,6 +187,14 @@ frontend: endpointUpdateReportedProductStocks: stockView/update-reported-product-stocks?ownMaterialNumber= # -- The endpoint for the partners BPNS endpointPartnerOwnSites: partners/ownSites + # -- The endpoint for the demand submodel + endpointDemand: demand + # -- The endpoint for the production submodel + endpointProduction: production + # -- The endpoint for the production range of the production submodel + endpointProductionRange: production/range + # -- The endpoint for the delivery submodel + endpointDelivery: delivery keycloak: # -- Disable the Keycloak integration. disabled: true diff --git a/frontend/.env b/frontend/.env index ea5ee400..eb431426 100644 --- a/frontend/.env +++ b/frontend/.env @@ -16,6 +16,7 @@ VITE_ENDPOINT_PARTNER_OWNSITES=partners/ownSites VITE_ENDPOINT_DEMAND=demand VITE_ENDPOINT_PRODUCTION=production VITE_ENDPOINT_PRODUCTION_RANGE=production/range +VITE_ENDPOINT_DELIVERY=delivery VITE_IDP_DISABLE=true VITE_IDP_URL=http://localhost:10081/ diff --git a/frontend/.env.dockerbuild b/frontend/.env.dockerbuild index 77bcbb7d..e7e88171 100644 --- a/frontend/.env.dockerbuild +++ b/frontend/.env.dockerbuild @@ -15,6 +15,7 @@ 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_ENDPOINT_DELIVERY=\$ENDPOINT_DELIVERY VITE_IDP_DISABLE=\$IDP_DISABLE VITE_IDP_URL=\$IDP_URL diff --git a/frontend/DEPENDENCIES b/frontend/DEPENDENCIES index 09e994b8..cc86c023 100644 --- a/frontend/DEPENDENCIES +++ b/frontend/DEPENDENCIES @@ -172,7 +172,7 @@ npm/npmjs/-/path-key/3.1.1, MIT, approved, clearlydefined npm/npmjs/-/path-parse/1.0.7, MIT, approved, clearlydefined npm/npmjs/-/path-scurry/1.10.1, BlueOak-1.0.0, approved, #9370 npm/npmjs/-/path-type/4.0.0, MIT, approved, clearlydefined -npm/npmjs/-/picocolors/1.0.0, ISC, approved, clearlydefined +npm/npmjs/-/picocolors/1.0.0, ISC, approved, #14718 npm/npmjs/-/picomatch/2.3.1, MIT, approved, clearlydefined npm/npmjs/-/pify/2.3.0, MIT, approved, clearlydefined npm/npmjs/-/pirates/4.0.6, MIT, approved, #680 diff --git a/frontend/src/components/ui/DateTime.tsx b/frontend/src/components/ui/DateTime.tsx index 4c917f2c..1b8f9a3e 100644 --- a/frontend/src/components/ui/DateTime.tsx +++ b/frontend/src/components/ui/DateTime.tsx @@ -80,7 +80,7 @@ export const DateTime = ({ error, value, onValueChange, ...props }: DateTimeProp handleDateChange(event)} diff --git a/frontend/src/config.json b/frontend/src/config.json index 3b765b5b..598f7a1d 100644 --- a/frontend/src/config.json +++ b/frontend/src/config.json @@ -13,6 +13,10 @@ "ENDPOINT_UPDATE_REPORTED_MATERIAL_STOCKS":"$ENDPOINT_UPDATE_REPORTED_MATERIAL_STOCKS", "ENDPOINT_UPDATE_REPORTED_PRODUCT_STOCKS":"$ENDPOINT_UPDATE_REPORTED_MATERIAL_STOCKS", "ENDPOINT_PARTNER_OWNSITES": "$ENDPOINT_PARTNER_OWNSITES", + "ENDPOINT_DEMAND": "$ENDPOINT_DEMAND", + "ENDPOINT_PRODUCTION": "$ENDPOINT_PRODUCTION", + "ENDPOINT_PRODUCTION_RANGE": "$ENDPOINT_PRODUCTION_RANGE", + "ENDPOINT_DELIVERY": "$ENDPOINT_DELIVERY", "IDP_DISABLE": "$IDP_DISABLE", "IDP_URL": "$IDP_URL", "IDP_REALM": "$IDP_REALM", diff --git a/frontend/src/features/dashboard/components/Dashboard.tsx b/frontend/src/features/dashboard/components/Dashboard.tsx index d46b0cd4..36992a37 100644 --- a/frontend/src/features/dashboard/components/Dashboard.tsx +++ b/frontend/src/features/dashboard/components/Dashboard.tsx @@ -41,7 +41,12 @@ import { PlannedProductionModal } from './PlannedProductionModal'; import { useProduction } from '../hooks/useProduction'; import { useReportedProduction } from '../hooks/useReportedProduction'; -import { refreshPartnerStocks } from '@services/stocks-service'; +import { requestReportedStocks } from '@services/stocks-service'; +import { useDelivery } from '../hooks/useDelivery'; +import { requestReportedDeliveries } from '@services/delivery-service'; +import { requestReportedProductions } from '@services/productions-service'; +import { requestReportedDemands } from '@services/demands-service'; +import { ModalMode } from '@models/types/data/modal-mode'; const NUMBER_OF_DAYS = 28; @@ -49,9 +54,9 @@ 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' }; + deliveryDialogOptions: { open: boolean; mode: ModalMode, direction: 'incoming' | 'outgoing', site: Site | null }; + demandDialogOptions: { open: boolean; mode: ModalMode }; + productionDialogOptions: { open: boolean; mode: ModalMode }; delivery: Delivery | null; demand: Partial | null; production: Partial | null; @@ -71,7 +76,7 @@ const initialState: DashboardState = { selectedMaterial: null, selectedSite: null, selectedPartnerSites: null, - deliveryDialogOptions: { open: false, mode: 'create' }, + deliveryDialogOptions: { open: false, mode: 'edit', direction: 'incoming', site: null }, demandDialogOptions: { open: false, mode: 'edit' }, productionDialogOptions: { open: false, mode: 'edit' }, delivery: null, @@ -83,7 +88,7 @@ const initialState: DashboardState = { export const Dashboard = ({ type }: { type: 'customer' | 'supplier' }) => { const [state, dispatch] = useReducer(reducer, initialState); const { stocks } = useStocks(type === 'customer' ? 'material' : 'product'); - const { partnerStocks, refreshPartnerStocks: refresh } = usePartnerStocks( + const { partnerStocks } = usePartnerStocks( type === 'customer' ? 'material' : 'product', state.selectedMaterial?.ownMaterialNumber ?? null ); @@ -94,25 +99,37 @@ export const Dashboard = ({ type }: { type: 'customer' | 'supplier' }) => { state.selectedSite?.bpns ?? null ); const { reportedProductions } = useReportedProduction(state.selectedMaterial?.ownMaterialNumber ?? null); + const { deliveries, refreshDelivery } = useDelivery( + state.selectedMaterial?.ownMaterialNumber ?? null, + state.selectedSite?.bpns ?? 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 })); + Promise.all([ + requestReportedStocks(type === 'customer' ? 'material' : 'product', state.selectedMaterial?.ownMaterialNumber ?? null), + requestReportedDeliveries(state.selectedMaterial?.ownMaterialNumber ?? null), + type === 'customer' + ? requestReportedProductions(state.selectedMaterial?.ownMaterialNumber ?? null) + : requestReportedDemands(state.selectedMaterial?.ownMaterialNumber ?? null) + ]).finally(() => dispatch({ type: 'isRefreshing', payload: false })); }; - const openDeliveryDialog = (d: Partial) => { + const openDeliveryDialog = useCallback( + (d: Partial, mode: ModalMode, direction: 'incoming' | 'outgoing' = 'outgoing', site: Site | null) => { + d.ownMaterialNumber = state.selectedMaterial?.ownMaterialNumber ?? ''; dispatch({ type: 'delivery', payload: d }); - dispatch({ type: 'deliveryDialogOptions', payload: { open: true, mode: 'edit' } }); - }; - const openDemandDialog = (d: Partial, mode: 'create' | 'edit') => { + dispatch({ type: 'deliveryDialogOptions', payload: { open: true, mode, direction, site } }); + }, + [state.selectedMaterial?.ownMaterialNumber] + ); + const openDemandDialog = (d: Partial, mode: ModalMode) => { 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') => { + const openProductionDialog = (p: Partial, mode: ModalMode) => { p.material ??= { materialFlag: true, productFlag: false, @@ -154,18 +171,20 @@ export const Dashboard = ({ type }: { type: 'customer' | 'supplier' }) => { numberOfDays={NUMBER_OF_DAYS} stocks={stocks ?? []} site={state.selectedSite} - onDeliveryClick={openDeliveryDialog} + onDeliveryClick={(delivery, mode) => openDeliveryDialog(delivery, mode, 'outgoing', state.selectedSite)} onProductionClick={openProductionDialog} productions={productions ?? []} + deliveries={deliveries ?? []} /> ) : ( openDeliveryDialog(delivery, mode, 'incoming', state.selectedSite)} onDemandClick={openDemandDialog} demands={demands} + deliveries={deliveries ?? []} /> ) ) : ( @@ -209,9 +228,10 @@ export const Dashboard = ({ type }: { type: 'customer' | 'supplier' }) => { numberOfDays={NUMBER_OF_DAYS} stocks={partnerStocks} site={ps} - onDeliveryClick={openDeliveryDialog} + onDeliveryClick={(delivery, mode) => openDeliveryDialog(delivery, mode, 'incoming', ps)} onDemandClick={openDemandDialog} demands={reportedDemands?.filter((d) => d.demandLocationBpns === ps.bpns) ?? []} + deliveries={deliveries ?? []} readOnly /> ) : ( @@ -220,9 +240,10 @@ export const Dashboard = ({ type }: { type: 'customer' | 'supplier' }) => { numberOfDays={NUMBER_OF_DAYS} stocks={partnerStocks ?? []} site={ps} - onDeliveryClick={openDeliveryDialog} + onDeliveryClick={(delivery, mode) => openDeliveryDialog(delivery, mode, 'outgoing', ps)} onProductionClick={openProductionDialog} productions={reportedProductions?.filter((p) => p.productionSiteBpns === ps.bpns) ?? []} + deliveries={deliveries ?? []} readOnly /> ) @@ -236,27 +257,28 @@ export const Dashboard = ({ type }: { type: 'customer' | 'supplier' }) => { )} - - 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 ?? []} + demands={(state.demandDialogOptions.mode === 'view' ? reportedDemands : demands) ?? []} /> dispatch({ type: 'productionDialogOptions', payload: { open: false, mode: state.productionDialogOptions.mode } })} onSave={refreshProduction} production={state.production} - productions={state.productionDialogOptions.mode === 'edit' ? productions ?? [] : []} + productions={(state.productionDialogOptions.mode === 'view' ? reportedProductions : productions) ?? []} + /> + + dispatch({ type: 'deliveryDialogOptions', payload: { ...state.deliveryDialogOptions, open: false, } }) + } + onSave={refreshDelivery} + delivery={state.delivery} + deliveries={deliveries ?? []} /> ); diff --git a/frontend/src/features/dashboard/components/DeliveryInformationModal.tsx b/frontend/src/features/dashboard/components/DeliveryInformationModal.tsx index 09a624e6..4c8fb3cc 100644 --- a/frontend/src/features/dashboard/components/DeliveryInformationModal.tsx +++ b/frontend/src/features/dashboard/components/DeliveryInformationModal.tsx @@ -17,9 +17,21 @@ under the License. SPDX-License-Identifier: Apache-2.0 */ - +import { Input, PageSnackbar, PageSnackbarStack, Table } from '@catena-x/portal-shared-components'; +import { DateTime } from '@components/ui/DateTime'; +import { usePartners } from '@features/stock-view/hooks/usePartners'; +import { UNITS_OF_MEASUREMENT } from '@models/constants/uom'; import { Delivery } from '@models/types/data/delivery'; -import { Button, Dialog, DialogTitle, Grid, Stack, Typography } from '@mui/material'; +import { Close, Delete, Save } from '@mui/icons-material'; +import { Autocomplete, Box, Button, Dialog, DialogTitle, Grid, Stack, Typography, capitalize } from '@mui/material'; +import { deleteDelivery, postDelivery } from '@services/delivery-service'; +import { getUnitOfMeasurement, isValidOrderReference } from '@util/helpers'; +import { useEffect, useMemo, useState } from 'react'; +import { Notification } from '@models/types/data/notification'; +import { INCOTERMS } from '@models/constants/incoterms'; +import { ARRIVAL_TYPES, DEPARTURE_TYPES } from '@models/constants/event-type'; +import { ModalMode } from '@models/types/data/modal-mode'; +import { Site } from '@models/types/edc/site'; const GridItem = ({ label, value }: { label: string; value: string }) => ( @@ -34,30 +46,572 @@ const GridItem = ({ label, value }: { label: string; value: string }) => ( ); +const createDeliveryColumns = (handleDelete: (row: Delivery) => void) => { + return [ + { + field: 'dateOfDeparture', + headerName: 'Departure Time', + headerAlign: 'center', + width: 150, + renderCell: (data: { row: Delivery }) => { + return ( + + {new Date(data.row.dateOfDeparture!).toLocaleString('de-DE', { + day: '2-digit', + month: '2-digit', + year: '2-digit', + hour: '2-digit', + minute: '2-digit', + })} + ({data.row.departureType.split('-')[0]}) + + ); + }, + }, + { + field: 'dateofArrival', + headerName: 'Arrival Time', + headerAlign: 'center', + width: 150, + renderCell: (data: { row: Delivery }) => { + return ( + + {new Date(data.row.dateOfArrival!).toLocaleString('de-DE', { + day: '2-digit', + month: '2-digit', + year: '2-digit', + hour: '2-digit', + minute: '2-digit', + })} + ({data.row.departureType.split('-')[0]}) + + ); + }, + }, + { + field: 'quantity', + headerName: 'Quantity', + headerAlign: 'center', + width: 120, + renderCell: (data: { row: Delivery }) => { + return ( + + {`${data.row.quantity} ${getUnitOfMeasurement(data.row.measurementUnit)}`} + + ); + }, + }, + { + field: 'partnerBpnl', + headerName: 'Partner', + headerAlign: 'center', + width: 200, + renderCell: (data: { row: Delivery }) => { + return ( + + {data.row.partnerBpnl} + + ); + }, + }, + { + field: 'customerOrderNumber', + headerName: 'Order Reference', + sortable: false, + headerAlign: 'center', + width: 200, + renderCell: (data: { row: Delivery }) => { + return ( + + {data.row.customerOrderNumber ? ( + <> + {`${data.row.customerOrderNumber} / ${data.row.customerOrderPositionNumber}`} + {data.row.supplierOrderNumber} + + ) : ( + '-' + )} + + ); + }, + }, + { + field: 'trackingNumber', + headerName: 'Tracking Number', + headerAlign: 'center', + width: 130, + renderCell: (data: { row: Delivery }) => { + return ( + + {data.row.trackingNumber} + + ); + }, + }, + { + field: 'incoterm', + headerName: 'Incoterm', + headerAlign: 'center', + width: 150, + renderCell: (data: { row: Delivery }) => { + return ( + + {INCOTERMS.find((i) => i.key === data.row.incoterm)?.value ?? '-'} + ({data.row.incoterm}) + + ); + }, + }, + { + field: 'delete', + headerName: '', + sortable: false, + disableColumnMenu: true, + headerAlign: 'center', + type: 'string', + width: 30, + renderCell: (data: { row: Delivery }) => { + return ( + + {!data.row.reported && } + + ); + }, + }, + ] as const; +}; + +const isValidDelivery = (delivery: Partial) => + delivery.ownMaterialNumber && + delivery.originBpns && + delivery.partnerBpnl && + delivery.destinationBpns && + delivery.quantity && + delivery.measurementUnit && + delivery.incoterm && + delivery.dateOfDeparture && + delivery.dateOfArrival && + delivery.departureType && + delivery.arrivalType && + delivery.dateOfArrival >= delivery.dateOfDeparture && + (delivery.departureType !== 'actual-departure' || delivery.dateOfDeparture <= new Date()) && + (delivery.arrivalType !== 'actual-arrival' || delivery.dateOfArrival <= new Date()) && + isValidOrderReference(delivery); + type DeliveryInformationModalProps = { open: boolean; + mode: ModalMode; + direction: 'incoming' | 'outgoing'; + site: Site | null; onClose: () => void; + onSave: () => void; delivery: Delivery | null; + deliveries: Delivery[]; }; -export const DeliveryInformationModal = ({ open, onClose, delivery }: DeliveryInformationModalProps) => { +export const DeliveryInformationModal = ({ + open, + mode, + direction, + site, + onClose, + onSave, + delivery, + deliveries, +}: DeliveryInformationModalProps) => { + const [temporaryDelivery, setTemporaryDelivery] = useState>(delivery ?? {}); + const { partners } = usePartners('product', temporaryDelivery?.ownMaterialNumber ?? null); + const [notifications, setNotifications] = useState([]); + const [formError, setFormError] = useState(false); + const dailyDeliveries = useMemo( + () => + deliveries?.filter( + (d) => + (direction === 'incoming' && d.destinationBpns === site?.bpns && new Date (d.dateOfArrival).toLocaleDateString() === new Date (delivery!.dateOfArrival).toLocaleDateString()) || + (direction === 'outgoing' && d.originBpns === site?.bpns && new Date (d.dateOfDeparture).toLocaleDateString() === new Date (delivery!.dateOfDeparture).toLocaleDateString()) + ) ?? [], + [deliveries, delivery, direction, site?.bpns] + ); + + const handleSaveClick = () => { + temporaryDelivery.customerOrderNumber ||= undefined; + temporaryDelivery.customerOrderPositionNumber ||= undefined; + temporaryDelivery.supplierOrderNumber ||= undefined; + if (!isValidDelivery(temporaryDelivery)) { + setFormError(true); + return; + } + setFormError(false); + + postDelivery(temporaryDelivery) + .then(() => { + onSave(); + setNotifications((ns) => [ + ...ns, + { + title: 'Delivery Added', + description: 'The Delivery has been added', + severity: 'success', + }, + ]); + }) + .catch((error) => { + setNotifications((ns) => [ + ...ns, + { + title: error.status === 409 ? 'Conflict' : 'Error requesting update', + description: error.status === 409 ? 'Delivery conflicting with an existing one' : error.error, + severity: 'error', + }, + ]); + }) + .finally(() => onClose()); + }; + + const handleDelete = (row: Delivery) => { + if (row.uuid) deleteDelivery(row.uuid).then(onSave); + }; + + useEffect(() => { + if (delivery) { + setTemporaryDelivery(delivery); + } + }, [delivery]); return ( - - Delivery Information - - - - - - - - - - - - - + <> + + + {capitalize(mode)} Delivery Information + + + + {mode === 'create' ? ( + <> + + + + option.value ?? ''} + isOptionEqualToValue={(option, value) => option?.key === value.key} + onChange={(_, value) => + setTemporaryDelivery({ ...temporaryDelivery, departureType: value?.key ?? undefined }) + } + value={DEPARTURE_TYPES.find((dt) => dt.key === temporaryDelivery.departureType) ?? null} + renderInput={(params) => ( + + )} + > + + + option.value ?? ''} + isOptionEqualToValue={(option, value) => option?.key === value.key} + onChange={(_, value) => + setTemporaryDelivery({ ...temporaryDelivery, arrivalType: value?.key ?? undefined }) + } + value={ARRIVAL_TYPES.find((dt) => dt.key === temporaryDelivery.arrivalType) ?? null} + renderInput={(params) => ( + + )} + > + + + new Date()) || + (!!temporaryDelivery.dateOfArrival && temporaryDelivery.dateOfArrival < temporaryDelivery.dateOfDeparture) + )} + value={temporaryDelivery?.dateOfDeparture ?? null} + onValueChange={(date) => + setTemporaryDelivery({ ...temporaryDelivery, dateOfDeparture: date ?? undefined }) + } + /> + + + new Date()) + )} + value={temporaryDelivery?.dateOfArrival ?? null} + onValueChange={(date) => + setTemporaryDelivery({ ...temporaryDelivery, dateOfArrival: date ?? undefined }) + } + /> + + + option?.name ?? ''} + renderInput={(params) => ( + + )} + onChange={(_, value) => + setTemporaryDelivery({ ...temporaryDelivery, partnerBpnl: value?.bpnl ?? undefined }) + } + value={partners?.find((p) => p.bpnl === temporaryDelivery.partnerBpnl) ?? null} + isOptionEqualToValue={(option, value) => option?.bpnl === value?.bpnl} + /> + + + s.bpnl === temporaryDelivery?.partnerBpnl)?.sites ?? []} + getOptionLabel={(option) => option.name ?? ''} + disabled={!temporaryDelivery?.partnerBpnl} + isOptionEqualToValue={(option, value) => option?.bpns === value.bpns} + onChange={(_, value) => + setTemporaryDelivery({ ...temporaryDelivery, ...(direction === 'incoming' ? { originBpns: value?.bpns ?? undefined } : { destinationBpns: value?.bpns ?? undefined }) }) + } + value={ + partners + ?.find((s) => s.bpnl === temporaryDelivery?.partnerBpnl) + ?.sites.find((s) => ( + direction === 'incoming' + ? s.bpns === temporaryDelivery.originBpns + : s.bpns === temporaryDelivery.destinationBpns) ?? '' + ) ?? null + } + renderInput={(params) => ( + + )} + /> + + + + setTemporaryDelivery((curr) => ({ + ...curr, + quantity: e.target.value ? parseFloat(e.target.value) : undefined, + })) + } + /> + + + option?.value ?? ''} + renderInput={(params) => ( + + )} + onChange={(_, value) => setTemporaryDelivery((curr) => ({ ...curr, measurementUnit: value?.key }))} + isOptionEqualToValue={(option, value) => option?.key === value?.key} + /> + + + + setTemporaryDelivery({ ...temporaryDelivery, trackingNumber: event.target.value }) + } + /> + + + i.key === temporaryDelivery.incoterm) ?? null + : null + } + options={INCOTERMS.filter((i) => direction === 'incoming' ? i.responsibility !== 'supplier' : i.responsibility !== 'customer')} + getOptionLabel={(option) => option?.value ?? ''} + renderInput={(params) => ( + + )} + onChange={(_, value) => setTemporaryDelivery((curr) => ({ ...curr, incoterm: value?.key }))} + isOptionEqualToValue={(option, value) => option?.key === value?.key} + /> + + + + setTemporaryDelivery({ ...temporaryDelivery, customerOrderNumber: event.target.value }) + } + /> + + + + setTemporaryDelivery({ + ...temporaryDelivery, + customerOrderPositionNumber: event.target.value, + }) + } + /> + + + + setTemporaryDelivery({ ...temporaryDelivery, supplierOrderNumber: event.target.value }) + } + /> + + + ) : ( + + { + row.uuid} + columns={createDeliveryColumns(handleDelete)} + rows={dailyDeliveries} + hideFooter + /> + } + + )} + + + + {mode === 'create' && ( + + )} + + + + + {notifications.map((notification, index) => ( + setNotifications((ns) => ns.filter((_, i) => i !== index) ?? [])} + /> + ))} + + ); }; diff --git a/frontend/src/features/dashboard/components/DemandCategoryModal.tsx b/frontend/src/features/dashboard/components/DemandCategoryModal.tsx index cf792b61..deea67c9 100644 --- a/frontend/src/features/dashboard/components/DemandCategoryModal.tsx +++ b/frontend/src/features/dashboard/components/DemandCategoryModal.tsx @@ -28,6 +28,7 @@ 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'; +import { ModalMode } from '@models/types/data/modal-mode'; const GridItem = ({ label, value }: { label: string; value: string }) => ( @@ -42,8 +43,8 @@ const GridItem = ({ label, value }: { label: string; value: string }) => ( ); -const createDemandColumns = (handleDelete: (row: Demand) => void) => - [ +const createDemandColumns = (handleDelete?: (row: Demand) => void) => { + const columns = [ { field: 'quantity', headerName: 'Quantity', @@ -108,7 +109,9 @@ const createDemandColumns = (handleDelete: (row: Demand) => void) => ); }, }, - { + ] as const; + if (handleDelete) { + return [...columns, { field: 'delete', headerName: '', sortable: false, @@ -125,14 +128,16 @@ const createDemandColumns = (handleDelete: (row: Demand) => void) => ); }, - }, - ] as const; + }] as const; + } + return columns; +}; type DemandCategoryModalProps = { open: boolean; demand: Partial | null; demands: Demand[] | null; - mode: 'create' | 'edit'; + mode: ModalMode; onClose: () => void; onSave: () => void; }; @@ -348,7 +353,7 @@ export const DemandCategoryModal = ({ open, mode, onClose, onSave, demand, deman }`} density="standard" getRowId={(row) => row.uuid} - columns={createDemandColumns(handleDelete)} + columns={createDemandColumns(mode === 'view' ? undefined : handleDelete)} rows={dailyDemands ?? []} hideFooter /> diff --git a/frontend/src/features/dashboard/components/DemandTable.tsx b/frontend/src/features/dashboard/components/DemandTable.tsx index a2164d08..9ab14e9b 100644 --- a/frontend/src/features/dashboard/components/DemandTable.tsx +++ b/frontend/src/features/dashboard/components/DemandTable.tsx @@ -26,6 +26,7 @@ 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'; +import { ModalMode } from '@models/types/data/modal-mode'; const createDemandRow = (numberOfDays: number, demands: Demand[]) => { return { @@ -40,15 +41,22 @@ const createDemandRow = (numberOfDays: number, demands: Demand[]) => { }; }; -const createDeliveryRow = (numberOfDays: number) => { +const createDeliveryRow = (numberOfDays: number, deliveries: Delivery[], site: Site) => { return { - ...Object.keys(Array.from({ length: numberOfDays })).reduce((acc, _, index) => ({ ...acc, [index]: 0 }), {}), + ...Object.keys(Array.from({ length: numberOfDays })).reduce((acc, _, index) => { + const date = new Date(); + date.setDate(date.getDate() + index); + const delivery = deliveries + .filter((d) => new Date(d.dateOfArrival ?? Date.now()).toDateString() === date.toDateString() && d.destinationBpns === site.bpns) + .reduce((sum, d) => sum + d.quantity, 0); + return { ...acc, [index]: delivery }; + }, {}), }; } -const createTableRows = (numberOfDays: number, stocks: Stock[], demands: Demand[], site: Site) => { +const createTableRows = (numberOfDays: number, stocks: Stock[], demands: Demand[], deliveries: Delivery[], site: Site) => { const demandRow = createDemandRow(numberOfDays, demands); - const deliveryRow = createDeliveryRow(numberOfDays); + const deliveryRow = createDeliveryRow(numberOfDays, deliveries, site); const currentStock = stocks.find((s) => s.stockLocationBpns === site.bpns)?.quantity ?? 0; const itemStock = { ...Object.keys(Array.from({ length: numberOfDays })).reduce( @@ -73,13 +81,14 @@ type DemandTableProps = { numberOfDays: number; stocks: Stock[] | null; demands: Demand[] | null; + deliveries: Delivery[] | null; site: Site; readOnly?: boolean; - onDeliveryClick: (delivery: Partial) => void; - onDemandClick: (demand: Partial, mode: 'create' | 'edit') => void; + onDeliveryClick: (delivery: Partial, mode: ModalMode) => void; + onDemandClick: (demand: Partial, mode: ModalMode) => void; }; -export const DemandTable = ({ numberOfDays, stocks, demands, site, readOnly, onDeliveryClick, onDemandClick }: DemandTableProps) => { +export const DemandTable = ({ numberOfDays, stocks, demands, deliveries, site, readOnly, onDeliveryClick, onDemandClick }: DemandTableProps) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const handleCellClick = (cellData: any) => { if (cellData.value === 0) return; @@ -87,21 +96,16 @@ export const DemandTable = ({ numberOfDays, stocks, demands, site, readOnly, onD case 'delivery': onDeliveryClick({ quantity: cellData.value, - etd: cellData.colDef.headerName, - origin: { - bpns: site?.bpns, - }, - destination: { - bpns: site?.bpns, - }, - }); + dateOfArrival: cellData.colDef.headerName, + destinationBpns: site.bpns, + }, readOnly ? 'view' : 'edit'); break; case 'demand': onDemandClick({ quantity: parseFloat(cellData.value), demandLocationBpns: site.bpns, day: new Date(cellData.colDef.headerName), - }, 'edit'); + }, readOnly ? 'view' : 'edit'); break; default: break; @@ -114,23 +118,20 @@ export const DemandTable = ({ numberOfDays, stocks, demands, site, readOnly, onD Site: {site.name} ({site.bpns}) - {!readOnly && } + {!readOnly && + + + } row.id} hideFooter={true} diff --git a/frontend/src/features/dashboard/components/PlannedProductionModal.tsx b/frontend/src/features/dashboard/components/PlannedProductionModal.tsx index 7515f5ae..4cfaac1b 100644 --- a/frontend/src/features/dashboard/components/PlannedProductionModal.tsx +++ b/frontend/src/features/dashboard/components/PlannedProductionModal.tsx @@ -28,6 +28,7 @@ import { deleteProduction, postProductionRange } from '@services/productions-ser import { Notification } from '@models/types/data/notification'; import { Close, Delete, Save } from '@mui/icons-material'; import { DateTime } from '@components/ui/DateTime'; +import { ModalMode } from '@models/types/data/modal-mode'; const GridItem = ({ label, value }: { label: string; value: string }) => ( @@ -42,8 +43,8 @@ const GridItem = ({ label, value }: { label: string; value: string }) => ( ); -const createProductionColumns = (handleDelete: (row: Production) => void) => - [ +const createProductionColumns = (handleDelete?: (row: Production) => void) => { + const columns = [ { field: 'estimatedTimeOfCompletion', headerName: 'Completion Time', @@ -95,27 +96,33 @@ const createProductionColumns = (handleDelete: (row: Production) => void) => ), }, - { + ] as const; + if (handleDelete) { + return [...columns, { field: 'delete', headerName: '', sortable: false, - filterable: false, - hideable: false, + disableColumnMenu: true, headerAlign: 'center', + type: 'string', width: 30, - renderCell: (data: { row: Production }) => ( - - - - ), - }, - ] as const; + renderCell: (data: { row: Production }) => { + return ( + + + + ); + }, + }] as const; + } + return columns; +}; type PlannedProductionModalProps = { open: boolean; - mode: 'create' | 'edit'; + mode: ModalMode; onClose: () => void; onSave: () => void; production: Partial | null; @@ -328,7 +335,7 @@ export const PlannedProductionModal = ({ open, mode, onClose, onSave, production : '' }`} getRowId={(row) => row.uuid} - columns={createProductionColumns(handleDelete)} + columns={createProductionColumns(mode === 'view' ? undefined : handleDelete)} rows={dailyProductions} hideFooter density="standard" diff --git a/frontend/src/features/dashboard/components/ProductionTable.tsx b/frontend/src/features/dashboard/components/ProductionTable.tsx index d3c5fe62..51792bbb 100644 --- a/frontend/src/features/dashboard/components/ProductionTable.tsx +++ b/frontend/src/features/dashboard/components/ProductionTable.tsx @@ -26,6 +26,7 @@ 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'; +import { ModalMode } from '@models/types/data/modal-mode'; const createProductionRow = (numberOfDays: number, productions: Production[]) => { return { @@ -33,19 +34,38 @@ const createProductionRow = (numberOfDays: number, productions: Production[]) => const date = new Date(); date.setDate(date.getDate() + index); const prod = productions - .filter( - (production) => new Date(production.estimatedTimeOfCompletion).toDateString() === date.toDateString() - ) + .filter((production) => new Date(production.estimatedTimeOfCompletion).toDateString() === date.toDateString()) .reduce((sum, production) => sum + production.quantity, 0); return { ...acc, [index]: prod }; }, {}), }; }; -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 createShipmentRow = (numberOfDays: number, deliveries: Delivery[], site: Site) => { + return { + ...Object.keys(Array.from({ length: numberOfDays })).reduce((acc, _, index) => { + const date = new Date(); + date.setDate(date.getDate() + index); + const d = deliveries + .filter( + (delivery) => + new Date(delivery.dateOfDeparture!).toDateString() === date.toDateString() && + delivery.originBpns === site.bpns + ) + .reduce((sum, delivery) => sum + delivery.quantity!, 0); + return { ...acc, [index]: d }; + }, {}), + }; +}; + +const createProductionTableRows = ( + numberOfDays: number, + stocks: Stock[], + productions: Production[], + deliveries: Delivery[], + site: Site +) => { + const shipmentRow = createShipmentRow(numberOfDays, deliveries, site); const productionRow = createProductionRow(numberOfDays, productions); const stockQuantity = stocks.filter((stock) => stock.stockLocationBpns === site.bpns).reduce((acc, stock) => acc + stock.quantity, 0); const stockRow = { @@ -56,9 +76,11 @@ const createProductionTableRows = (numberOfDays: number, stocks: Stock[], produc index === 0 ? stockQuantity : acc[(index - 1) as keyof typeof acc] - - shipmentRow[(index -1) as keyof typeof shipmentRow] + + shipmentRow[(index - 1) as keyof typeof shipmentRow] + productionRow[(index - 1) as keyof typeof productionRow], - }), {}), + }), + {} + ), }; return [ { id: 'shipment', name: 'Outgoing Shipments', ...shipmentRow }, @@ -66,49 +88,76 @@ const createProductionTableRows = (numberOfDays: number, stocks: Stock[], produc { id: 'plannedProduction', name: 'Planned Production', ...productionRow }, ]; }; + type ProductionTableProps = { - numberOfDays: number; - stocks: Stock[] | null; - site: Site; + numberOfDays: number; + stocks: Stock[] | null; + site: Site; productions: Production[] | null; + deliveries: Delivery[]; readOnly: boolean; - onDeliveryClick: (delivery: Delivery) => void - onProductionClick: (production: Partial, mode: 'create' | 'edit') => void; + onDeliveryClick: (delivery: Partial, mode: ModalMode) => void; + onProductionClick: (production: Partial, mode: ModalMode) => void; }; -export const ProductionTable = ({ numberOfDays, stocks, site, productions, readOnly, onDeliveryClick, onProductionClick, }: ProductionTableProps) => { +export const ProductionTable = ({ + numberOfDays, + stocks, + site, + productions, + deliveries, + readOnly, + onDeliveryClick, + onProductionClick, +}: ProductionTableProps) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const handleCellClick = (cellData: any) => { if (cellData.value === 0) return; if (cellData.id === 'shipment') { - onDeliveryClick({ - quantity: cellData.value, - etd: cellData.colDef.headerName, - origin: { - bpns: site?.bpns, - }, - destination: { - bpns: site?.bpns, + onDeliveryClick( + { + quantity: cellData.value, + dateOfDeparture: cellData.colDef.headerName, + originBpns: site.bpns, + destinationBpns: site.bpns, }, - }); + readOnly ? 'view' : 'edit' + ); } if (cellData.id === 'plannedProduction') { const material = stocks?.length ? stocks[0].material : undefined; - onProductionClick({ + onProductionClick( + { quantity: parseFloat(cellData.value), material, estimatedTimeOfCompletion: new Date(cellData.colDef.headerName), productionSiteBpns: site.bpns, - }, 'edit'); + }, + readOnly ? 'view' : 'edit' + ); } }; return ( - - Site: + + + {' '} + Site:{' '} + {site.name} ({site.bpns}) {!readOnly && ( - + + @@ -120,7 +169,7 @@ export const ProductionTable = ({ numberOfDays, stocks, site, productions, readO noRowsMsg="Select a Site to show production data" columns={createDateColumnHeaders(numberOfDays)} onCellClick={handleCellClick} - rows={site ? createProductionTableRows(numberOfDays, stocks ?? [], productions ?? [], site) : []} + rows={site ? createProductionTableRows(numberOfDays, stocks ?? [], productions ?? [], deliveries ?? [], site) : []} getRowId={(row) => row.id} hideFooter={true} /> diff --git a/frontend/src/features/dashboard/hooks/useDelivery.ts b/frontend/src/features/dashboard/hooks/useDelivery.ts new file mode 100644 index 00000000..b73a7701 --- /dev/null +++ b/frontend/src/features/dashboard/hooks/useDelivery.ts @@ -0,0 +1,43 @@ +/* +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 { Delivery } from '@models/types/data/delivery'; +import { BPNS } from '@models/types/edc/bpn'; + +export const useDelivery = (materialNumber: string | null, site: BPNS | null) => { + const { + data: deliveries, + error: deliveriesError, + isLoading: isLoadingDeliverys, + refresh: refreshDelivery, + } = useFetch( + materialNumber && site + ? `${config.app.BACKEND_BASE_URL}${config.app.ENDPOINT_DELIVERY}?ownMaterialNumber=${materialNumber}&site=${site}` + : undefined + ); + return { + deliveries, + deliveriesError, + isLoadingDeliverys, + refreshDelivery, + }; +}; diff --git a/frontend/src/features/dashboard/hooks/useReportedDemand.ts b/frontend/src/features/dashboard/hooks/useReportedDemand.ts index a6114d39..7f772c85 100644 --- a/frontend/src/features/dashboard/hooks/useReportedDemand.ts +++ b/frontend/src/features/dashboard/hooks/useReportedDemand.ts @@ -1,13 +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 { 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?ownMaterialNumber=${materialNumber}` : undefined); + const {data: reportedDemands, error: reportedDemandsError, isLoading: isLoadingReportedDemands, refresh: refreshReportedDemands } = useFetch(materialNumber ? `${config.app.BACKEND_BASE_URL}${config.app.ENDPOINT_DEMAND}/reported?ownMaterialNumber=${materialNumber}` : undefined); return { reportedDemands, reportedDemandsError, isLoadingReportedDemands, - refreshDemand, + refreshReportedDemands, }; } diff --git a/frontend/src/features/dashboard/util/helpers.tsx b/frontend/src/features/dashboard/util/helpers.tsx index 9a4b79fb..daf338da 100644 --- a/frontend/src/features/dashboard/util/helpers.tsx +++ b/frontend/src/features/dashboard/util/helpers.tsx @@ -19,6 +19,8 @@ SPDX-License-Identifier: Apache-2.0 */ import { Box, Button } from '@mui/material'; +const weekdays = ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa']; + export const createDateColumnHeaders = (numberOfDays: number) => { return Object.keys(Array.from({ length: numberOfDays })).map((_, index) => { const date = new Date(); @@ -29,11 +31,14 @@ export const createDateColumnHeaders = (numberOfDays: number) => { headerAlign: 'center' as const, sortable: false, disableColumnMenu: true, - width: 180, + width: 100, renderHeader: (data: { colDef: { headerName?: string } }) => { return ( - - {new Date(data.colDef.headerName!).toLocaleDateString(undefined, { weekday: 'short', day: '2-digit', month: '2-digit', year: 'numeric' })} + + {weekdays[date.getDay()]} + + {new Date(data.colDef.headerName!).toLocaleDateString(undefined, { day: '2-digit', month: '2-digit', year: 'numeric' })} + ); }, diff --git a/frontend/src/features/stock-view/components/StockDetailsView.tsx b/frontend/src/features/stock-view/components/StockDetailsView.tsx index d9dbbaec..d48d580e 100644 --- a/frontend/src/features/stock-view/components/StockDetailsView.tsx +++ b/frontend/src/features/stock-view/components/StockDetailsView.tsx @@ -22,7 +22,7 @@ import { useState } from 'react'; import { PageSnackbar, PageSnackbarStack } from '@catena-x/portal-shared-components'; import { Stock, StockType } from '@models/types/data/stock'; -import { postStocks, putStocks, refreshPartnerStocks } from '@services/stocks-service'; +import { postStocks, putStocks, requestReportedStocks } from '@services/stocks-service'; import { useMaterials } from '../hooks/useMaterials'; import { StockUpdateForm } from './StockUpdateForm'; @@ -41,7 +41,7 @@ export const StockDetailsView = ({ type }: StockDetailsView const { materials } = useMaterials(type); const { stocks, refreshStocks } = useStocks(type); const [selectedMaterial, setSelectedMaterial] = useState(null); - const { partnerStocks, refreshPartnerStocks: refresh } = usePartnerStocks( + const { partnerStocks } = usePartnerStocks( type, type === 'product' ? selectedMaterial?.material?.materialNumberSupplier : selectedMaterial?.material?.materialNumberCustomer ); @@ -52,7 +52,7 @@ export const StockDetailsView = ({ type }: StockDetailsView const handleStockRefresh = () => { setRefreshing(true); - refreshPartnerStocks( + requestReportedStocks( type, (type == 'product' ? selectedMaterial?.material?.materialNumberSupplier : selectedMaterial?.material?.materialNumberCustomer) ?? null @@ -67,14 +67,14 @@ export const StockDetailsView = ({ type }: StockDetailsView severity: 'success', }, ]); - refresh(); }) - .catch((error) => { + .catch((error: unknown) => { + const msg = error !== null && typeof error === 'object' && 'message' in error && typeof error.message === 'string' ? error.message : 'Unknown Error'; setNotifications((ns) => [ ...ns, { title: 'Error requesting update', - description: error.message, + description: msg, severity: 'error', }, ]); diff --git a/frontend/src/hooks/useFetch.ts b/frontend/src/hooks/useFetch.ts index 6593a5e9..8d27be01 100644 --- a/frontend/src/hooks/useFetch.ts +++ b/frontend/src/hooks/useFetch.ts @@ -64,7 +64,7 @@ export const useFetch = (url?: string, options?: RequestInit) => { }, [fetchData]); const refresh = () => { - fetchData(); + return fetchData(); } return { data, diff --git a/frontend/src/index.css b/frontend/src/index.css index 55adf860..ed2eb32c 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -29,6 +29,7 @@ body { [role='tabpanel'] { width: 100%; + width: 100%; } .MuiDataGrid-root > .MuiBox-root h5 > span { @@ -43,6 +44,7 @@ body { .table-container .MuiDataGrid-root > .MuiBox-root { height: 0; + height: 0; } .table-container .MuiDataGrid-root { @@ -59,7 +61,6 @@ input[type='time'] { padding: 0.875rem; border-radius: 4px; border: 1px solid #c5c7cb; - } input[type='time']:hover { @@ -77,3 +78,4 @@ input[type='time']:active { input[type='time'].error { border-color: #f44336; } + diff --git a/frontend/src/models/constants/config.ts b/frontend/src/models/constants/config.ts index b5a391d0..eac5c5c0 100644 --- a/frontend/src/models/constants/config.ts +++ b/frontend/src/models/constants/config.ts @@ -36,6 +36,7 @@ const app = { ENDPOINT_DEMAND: import.meta.env.VITE_ENDPOINT_DEMAND.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, + ENDPOINT_DELIVERY: import.meta.env.VITE_ENDPOINT_DELIVERY.trim() as string, }; const auth = { diff --git a/frontend/src/models/constants/event-type.ts b/frontend/src/models/constants/event-type.ts new file mode 100644 index 00000000..cbf89f41 --- /dev/null +++ b/frontend/src/models/constants/event-type.ts @@ -0,0 +1,41 @@ +/* +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 ARRIVAL_TYPES = [ + { + key: 'estimated-arrival', + value: 'Estimated' + }, + { + key: 'actual-arrival', + value: 'Actual' + } +] as const; + +export const DEPARTURE_TYPES = [ + { + key: 'estimated-departure', + value: 'Estimated' + }, + { + key: 'actual-departure', + value: 'Actual' + } +] as const; diff --git a/frontend/src/models/constants/incoterms.ts b/frontend/src/models/constants/incoterms.ts new file mode 100644 index 00000000..1474fe03 --- /dev/null +++ b/frontend/src/models/constants/incoterms.ts @@ -0,0 +1,83 @@ +/* +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 +*/ + +type Incoterm = { + key: string; + value: string; + responsibility: 'customer' | 'partial' | 'supplier'; +} + +export const INCOTERMS: Incoterm[] = [ + { + key: 'EXW', + value: 'EX Works', + responsibility: 'customer', + }, + { + key: 'FCA', + value: 'Free Carrier', + responsibility: 'partial', + }, + { + key: 'FAS', + value: 'Free Alongside Ship', + responsibility: 'partial', + }, + { + key: 'FOB', + value: 'Free On Board', + responsibility: 'partial', + }, + { + key: 'CFR', + value: 'Cost and Freight', + responsibility: 'partial', + }, + { + key: 'CIF', + value: 'Cost Insurance Freight', + responsibility: 'partial', + }, + { + key: 'DAP', + value: 'Delivered At Place', + responsibility: 'supplier', + }, + { + key: 'DPU', + value: 'Delivered at Place Unloaded', + responsibility: 'supplier', + }, + { + key: 'CPT', + value: 'Carriage Paid To', + responsibility: 'supplier', + }, + { + key: 'CIP', + value: 'Carriage Insurance Paid', + responsibility: 'supplier', + }, + { + key: 'DDP', + value: 'Delivered Duty Paid', + responsibility: 'supplier', + }, +]; diff --git a/frontend/src/models/types/data/delivery.ts b/frontend/src/models/types/data/delivery.ts index 8b83aa70..e5b07104 100644 --- a/frontend/src/models/types/data/delivery.ts +++ b/frontend/src/models/types/data/delivery.ts @@ -18,17 +18,29 @@ under the License. SPDX-License-Identifier: Apache-2.0 */ -import { BPNA, BPNS } from '../edc/bpn'; +import { UUID } from 'crypto'; +import { BPNA, BPNL, BPNS } from '../edc/bpn'; +import { UnitOfMeasurementKey } from './uom'; +import { OrderReference } from './order-reference'; + +export type ArrivalType = 'estimated-arrival' | 'actual-arrival'; +export type DepartureType = 'estimated-departure' | 'actual-departure'; export type Delivery = { - quantity?: number; - etd: string; - origin: { - bpns: BPNS; - bpna?: BPNA; - }, - destination: { - bpns: BPNS; - bpna?: BPNA; - }, -} + uuid?: UUID; + ownMaterialNumber: string; + quantity: number; + measurementUnit: UnitOfMeasurementKey; + trackingNumber: string; + incoterm: string; + partnerBpnl: BPNL; + destinationBpns: BPNS; + destinationBpna?: BPNA; + originBpns: BPNS; + originBpna?: BPNA; + dateOfDeparture: Date; + dateOfArrival: Date; + departureType: DepartureType; + arrivalType: ArrivalType; + reported: boolean; +} & OrderReference; diff --git a/frontend/src/models/types/data/modal-mode.ts b/frontend/src/models/types/data/modal-mode.ts new file mode 100644 index 00000000..c8f39f2e --- /dev/null +++ b/frontend/src/models/types/data/modal-mode.ts @@ -0,0 +1,21 @@ +/* +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 ModalMode = 'create' | 'edit' | 'view'; diff --git a/frontend/src/models/types/data/notification.ts b/frontend/src/models/types/data/notification.ts index 18939eb2..7b89d77e 100644 --- a/frontend/src/models/types/data/notification.ts +++ b/frontend/src/models/types/data/notification.ts @@ -17,6 +17,7 @@ under the License. SPDX-License-Identifier: Apache-2.0 */ + export type Notification = { title: string; description: string; diff --git a/frontend/src/services/delivery-service.ts b/frontend/src/services/delivery-service.ts new file mode 100644 index 00000000..b6f3ab33 --- /dev/null +++ b/frontend/src/services/delivery-service.ts @@ -0,0 +1,67 @@ +/* +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 { Delivery } from '@models/types/data/delivery'; +import { UUID } from 'crypto'; + +export const postDelivery = async (delivery: Partial) => { + const res = await fetch(config.app.BACKEND_BASE_URL + config.app.ENDPOINT_DELIVERY, { + method: 'POST', + body: JSON.stringify(delivery), + 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 deleteDelivery = async (id: UUID) => { + const res = await fetch(config.app.BACKEND_BASE_URL + config.app.ENDPOINT_DELIVERY + `/${id}`, { + method: 'DELETE', + headers: { + 'X-API-KEY': config.app.BACKEND_API_KEY, + }, + }); + if(res.status >= 400) { + const error = await res.json(); + throw error; + } +} + +export const requestReportedDeliveries = async (materialNumber: string | null) => { + const res = await fetch(`${config.app.BACKEND_BASE_URL}${config.app.ENDPOINT_DELIVERY}/reported/refresh?ownMaterialNumber=${materialNumber}`, { + method: 'GET', + 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(); +} diff --git a/frontend/src/services/demands-service.ts b/frontend/src/services/demands-service.ts index 3a2d53cb..9d2a6d40 100644 --- a/frontend/src/services/demands-service.ts +++ b/frontend/src/services/demands-service.ts @@ -50,3 +50,18 @@ export const deleteDemand = async (id: UUID) => { throw error; } } + +export const requestReportedDemands = async (materialNumber: string | null) => { + const res = await fetch(`${config.app.BACKEND_BASE_URL}${config.app.ENDPOINT_DEMAND}/reported/refresh?ownMaterialNumber=${materialNumber}`, { + method: 'GET', + 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(); +} diff --git a/frontend/src/services/productions-service.ts b/frontend/src/services/productions-service.ts index 3ae4741b..0f2a2ba3 100644 --- a/frontend/src/services/productions-service.ts +++ b/frontend/src/services/productions-service.ts @@ -49,3 +49,18 @@ export const deleteProduction = async (id: UUID) => { throw error; } } + +export const requestReportedProductions = async (materialNumber: string | null) => { + const res = await fetch(`${config.app.BACKEND_BASE_URL}${config.app.ENDPOINT_PRODUCTION}/reported/refresh?ownMaterialNumber=${materialNumber}`, { + method: 'GET', + 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(); +} diff --git a/frontend/src/services/stocks-service.ts b/frontend/src/services/stocks-service.ts index 33099660..0703fcf2 100644 --- a/frontend/src/services/stocks-service.ts +++ b/frontend/src/services/stocks-service.ts @@ -55,7 +55,7 @@ export const putStocks = async (type: StockType, stock: Stock) => { return res.json(); } -export const refreshPartnerStocks = async (type: StockType, materialNumber: string | null) => { +export const requestReportedStocks = async (type: StockType, materialNumber: string | null) => { const endpoint = type === 'product' ? config.app.ENDPOINT_UPDATE_REPORTED_PRODUCT_STOCKS : config.app.ENDPOINT_UPDATE_REPORTED_MATERIAL_STOCKS; const res = await fetch(`${config.app.BACKEND_BASE_URL}${endpoint}${materialNumber}`, { method: 'GET', diff --git a/frontend/src/util/helpers.ts b/frontend/src/util/helpers.ts index e36f0933..a71aa329 100644 --- a/frontend/src/util/helpers.ts +++ b/frontend/src/util/helpers.ts @@ -18,13 +18,19 @@ under the License. SPDX-License-Identifier: Apache-2.0 */ +import { INCOTERMS } from '@models/constants/incoterms'; import { UNITS_OF_MEASUREMENT } from '@models/constants/uom'; import { OrderReference } from '@models/types/data/order-reference'; +import { OrderReference } from '@models/types/data/order-reference'; import { UnitOfMeasurementKey } from '@models/types/data/uom'; export const getUnitOfMeasurement = (unitOfMeasurementKey: UnitOfMeasurementKey) => UNITS_OF_MEASUREMENT.find((uom) => uom.key === unitOfMeasurementKey)?.value; +export const getIncoterm = (incoterm: string) => { + return INCOTERMS.find((i) => i.key === incoterm)?.value; +} + export const getCatalogOperator = (operatorId: string) => { switch (operatorId) { case 'odrl:eq': diff --git a/local/docker-compose.yaml b/local/docker-compose.yaml index bba40fc6..604586e4 100644 --- a/local/docker-compose.yaml +++ b/local/docker-compose.yaml @@ -44,6 +44,7 @@ services: - ENDPOINT_DEMAND=demand - ENDPOINT_PRODUCTION=production - ENDPOINT_PRODUCTION_RANGE=production/range + - ENDPOINT_DELIVERY=delivery - IDP_DISABLE=true - NGINX_RATE_LIMIT=10m - NGINX_BURST=30 @@ -189,6 +190,7 @@ services: - ENDPOINT_DEMAND=demand - ENDPOINT_PRODUCTION=production - ENDPOINT_PRODUCTION_RANGE=production/range + - ENDPOINT_DELIVERY=delivery - IDP_DISABLE=true - NGINX_RATE_LIMIT=10m - NGINX_BURST=30