From a81a3684d5228b4d5c8af52cd72b56f76aa52bbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Schr=C3=B6der?= <131770181+ReneSchroederLJ@users.noreply.github.com> Date: Mon, 13 May 2024 07:38:42 +0200 Subject: [PATCH] feat: implemented delivery information in the frontend --- .../puris/templates/frontend-deployment.yaml | 10 +- charts/puris/values.yaml | 8 + frontend/.env | 1 + frontend/.env.dockerbuild | 1 + .../dashboard/components/Dashboard.tsx | 82 ++- .../components/DeliveryInformationModal.tsx | 577 +++++++++++++++++- .../components/DemandCategoryModal.tsx | 19 +- .../dashboard/components/DemandTable.tsx | 38 +- .../components/PlannedProductionModal.tsx | 39 +- .../dashboard/components/ProductionTable.tsx | 111 +++- .../features/dashboard/hooks/useDelivery.ts | 23 + .../dashboard/hooks/useReportedDelivery.ts | 13 + .../dashboard/hooks/useReportedDemand.ts | 4 +- .../src/features/dashboard/util/helpers.tsx | 11 +- .../components/StockDetailsView.tsx | 12 +- frontend/src/hooks/useFetch.ts | 2 +- frontend/src/index.css | 4 +- frontend/src/models/constants/config.ts | 1 + frontend/src/models/constants/event-type.ts | 21 + frontend/src/models/constants/incoterms.ts | 46 ++ frontend/src/models/types/data/delivery.ts | 35 +- frontend/src/models/types/data/modal-mode.ts | 1 + .../src/models/types/data/notification.ts | 1 + frontend/src/services/delivery-service.ts | 47 ++ frontend/src/services/demands-service.ts | 15 + frontend/src/services/productions-service.ts | 15 + frontend/src/services/stocks-service.ts | 2 +- frontend/src/util/helpers.ts | 6 + 28 files changed, 998 insertions(+), 147 deletions(-) create mode 100644 frontend/src/features/dashboard/hooks/useDelivery.ts create mode 100644 frontend/src/features/dashboard/hooks/useReportedDelivery.ts create mode 100644 frontend/src/models/constants/event-type.ts create mode 100644 frontend/src/models/constants/incoterms.ts create mode 100644 frontend/src/models/types/data/modal-mode.ts create mode 100644 frontend/src/services/delivery-service.ts 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/src/features/dashboard/components/Dashboard.tsx b/frontend/src/features/dashboard/components/Dashboard.tsx index d46b0cd4..cd9036af 100644 --- a/frontend/src/features/dashboard/components/Dashboard.tsx +++ b/frontend/src/features/dashboard/components/Dashboard.tsx @@ -41,7 +41,13 @@ 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 { useReportedDelivery } from '../hooks/useReportedDelivery'; +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 +55,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 +77,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 +89,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 +100,38 @@ 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 { reportedDeliveries } = useReportedDelivery(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 })); + 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 +173,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={reportedDeliveries} /> ) ) : ( @@ -209,9 +230,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 +242,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={reportedDeliveries ?? []} readOnly /> ) @@ -236,27 +259,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={(type === 'customer' ? reportedDeliveries : deliveries) ?? []} /> ); diff --git a/frontend/src/features/dashboard/components/DeliveryInformationModal.tsx b/frontend/src/features/dashboard/components/DeliveryInformationModal.tsx index 09a624e6..b28f3c9f 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 { getIncoterm, 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,555 @@ const GridItem = ({ label, value }: { label: string; value: string }) => ( ); +const createDeliveryColumns = (handleDelete?: (row: Delivery) => void) => { + const columns = [ + { + 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}) + + ); + }, + }, + ] as const; + if (handleDelete) { + return [ + ...columns, + { + field: 'delete', + headerName: '', + sortable: false, + disableColumnMenu: true, + headerAlign: 'center', + type: 'string', + width: 30, + renderCell: (data: { row: Delivery }) => { + return ( + + + + ); + }, + }, + ] as const; + } + return columns; +}; + +const isValidDelivery = (delivery: Partial) => + delivery.ownMaterialNumber && + delivery.originBpns && + delivery.partnerBpnl && + delivery.destinationBpns && + delivery.quantity && + delivery.measurementUnit && + delivery.dateOfDeparture && + delivery.dateOfArrival && + 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 saved 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) => ( + + )} + > + + + + setTemporaryDelivery({ ...temporaryDelivery, dateOfDeparture: date ?? undefined }) + } + /> + + + + 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, destinationBpns: value?.bpns ?? undefined }) + } + value={ + partners + ?.find((s) => s.bpnl === temporaryDelivery?.partnerBpnl) + ?.sites.find((s) => 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 }) + } + /> + + + 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(mode === 'view' ? undefined : 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..62a1e152 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; @@ -130,7 +134,7 @@ export const DemandTable = ({ numberOfDays, stocks, demands, site, readOnly, onD title="" noRowsMsg="Select a Material to show the customer demand" columns={createDateColumnHeaders(numberOfDays)} - rows={createTableRows(numberOfDays, stocks ?? [], demands ?? [], site)} + rows={createTableRows(numberOfDays, stocks ?? [], demands ?? [], deliveries ?? [], site)} onCellClick={handleCellClick} getRowId={(row) => 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..144ef167 --- /dev/null +++ b/frontend/src/features/dashboard/hooks/useDelivery.ts @@ -0,0 +1,23 @@ +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}?materialNumber=${materialNumber}&site=${site}` + : undefined + ); + return { + deliveries, + deliveriesError, + isLoadingDeliverys, + refreshDelivery, + }; +}; diff --git a/frontend/src/features/dashboard/hooks/useReportedDelivery.ts b/frontend/src/features/dashboard/hooks/useReportedDelivery.ts new file mode 100644 index 00000000..ae91f58c --- /dev/null +++ b/frontend/src/features/dashboard/hooks/useReportedDelivery.ts @@ -0,0 +1,13 @@ +import { useFetch } from '@hooks/useFetch'; +import { config } from '@models/constants/config'; +import { Delivery } from '@models/types/data/delivery'; + +export const useReportedDelivery = (materialNumber: string | null) => { + const {data: reportedDeliveries, error: reportedDeliveriesError, isLoading: isLoadingReportedDeliveries, refresh: refreshDelivery } = useFetch(materialNumber ? `${config.app.BACKEND_BASE_URL}${config.app.ENDPOINT_DELIVERY}/reported?materialNumber=${materialNumber}` : undefined); + return { + reportedDeliveries, + reportedDeliveriesError, + isLoadingReportedDeliveries, + refreshDelivery, + }; +} diff --git a/frontend/src/features/dashboard/hooks/useReportedDemand.ts b/frontend/src/features/dashboard/hooks/useReportedDemand.ts index a6114d39..7d0b378f 100644 --- a/frontend/src/features/dashboard/hooks/useReportedDemand.ts +++ b/frontend/src/features/dashboard/hooks/useReportedDemand.ts @@ -3,11 +3,11 @@ 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..0ff3b7ec --- /dev/null +++ b/frontend/src/models/constants/event-type.ts @@ -0,0 +1,21 @@ +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..07719603 --- /dev/null +++ b/frontend/src/models/constants/incoterms.ts @@ -0,0 +1,46 @@ +export const INCOTERMS = [ + { + key: 'EXW', + value: 'EX Works', + }, + { + key: 'FCA', + value: 'Free Carrier', + }, + { + key: 'FAS', + value: 'Free Alongside Ship', + }, + { + key: 'FOB', + value: 'Free On Board', + }, + { + key: 'CFR', + value: 'Cost and Freight', + }, + { + key: 'CIF', + value: 'Cost Insurance Freight', + }, + { + key: 'DAP', + value: 'Delivered At Place', + }, + { + key: 'DPU', + value: 'Delivered at Place Unloaded', + }, + { + key: 'CPT', + value: 'Carriage Paid To', + }, + { + key: 'CIP', + value: 'Carriage Insurance Paid', + }, + { + key: 'DDP', + value: 'Delivered Duty Paid', + }, +]; diff --git a/frontend/src/models/types/data/delivery.ts b/frontend/src/models/types/data/delivery.ts index 8b83aa70..6f90c634 100644 --- a/frontend/src/models/types/data/delivery.ts +++ b/frontend/src/models/types/data/delivery.ts @@ -18,17 +18,28 @@ 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; +} & 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..fb6c7c03 --- /dev/null +++ b/frontend/src/models/types/data/modal-mode.ts @@ -0,0 +1 @@ +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..63ccacb3 --- /dev/null +++ b/frontend/src/services/delivery-service.ts @@ -0,0 +1,47 @@ +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':