Adjusted stepper for shipment tracking

This commit is contained in:
GotthardG
2024-10-31 10:25:46 +01:00
parent 930d551464
commit d6d7e7c919
12 changed files with 959 additions and 239 deletions

View File

@ -11,6 +11,7 @@ interface DewarDetailsProps {
returnAddresses: Address[];
}
const DewarDetails: React.FC<DewarDetailsProps> = ({
dewar,
trackingNumber,
@ -23,7 +24,9 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
const updateSelectedDetails = (contactPerson?: { firstname: string }, returnAddress?: Address) => {
if (contactPerson) setSelectedContactPerson(contactPerson.firstname);
if (returnAddress) setSelectedReturnAddress(returnAddress.id.toString());
if (returnAddress?.id != null) {
setSelectedReturnAddress(returnAddress.id.toString());
};
};
React.useEffect(() => {
@ -132,7 +135,8 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
>
<MenuItem value="" disabled>Select Return Address</MenuItem>
{returnAddresses.map((address) => (
<MenuItem key={address.id} value={address.id.toString()}>{address.street}</MenuItem>
<MenuItem key={address.id ?? 'unknown'} value={address.id?.toString() ?? 'unknown'}>
{address.street} </MenuItem>
))}
<MenuItem value="add">Add New Return Address</MenuItem>
</Select>

View File

@ -2,20 +2,32 @@ import React from 'react';
import { Stepper, Step, StepLabel, StepIconProps, Typography } from '@mui/material';
import AirplanemodeActiveIcon from '@mui/icons-material/AirplanemodeActive';
import StoreIcon from '@mui/icons-material/Store';
import bottleIcon from '../assets/icons/bottle-svgrepo-com-grey.svg';
import RecycleIcon from '@mui/icons-material/Restore';
import { Dewar } from "../../openapi";
// Constants
const ICON_STYLE = { width: 24, height: 24 };
// Define the possible statuses
type DewarStatus = 'In Preparation' | 'Ready for Shipping' | 'Shipped' | 'Not Arrived' | 'Arrived' | 'Returned' | 'Delayed';
// Inline SVG Component
const BottleIcon: React.FC<{ fill: string }> = ({ fill }) => (
<svg fill={fill} height="24px" width="24px" version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 276.777 276.777">
<path d="M190.886,82.273c-3.23-2.586-7.525-7.643-7.525-21.639V43h8.027V0h-106v43h8.027v17.635c0,11.66-1.891,17.93-6.524,21.639 c-21.813,17.459-31.121,36.748-31.121,64.5v100.088c0,16.496,13.42,29.916,29.916,29.916h105.405 c16.496,0,29.916-13.42,29.916-29.916V146.773C221.007,121.103,210.029,97.594,190.886,82.273z M191.007,246.777H85.77V146.773 c0-18.589,5.199-29.339,19.867-41.078c15.758-12.612,17.778-30.706,17.778-45.061V43h29.945v17.635 c0,19.927,6.318,35.087,18.779,45.061c11.99,9.597,18.867,24.568,18.867,41.078V246.777z"/>
</svg>
);
// Define types for icons mapping.
const ICONS: { [key: number]: React.ReactElement } = {
0: <img src={bottleIcon} alt="Bottle Icon" style={ICON_STYLE} />,
1: <AirplanemodeActiveIcon style={ICON_STYLE} />,
2: <StoreIcon style={ICON_STYLE} />,
0: <BottleIcon fill="grey" />,
1: <AirplanemodeActiveIcon style={{ ...ICON_STYLE, color: 'blue' }} />, // 'Ready for Shipping' -> Active
2: <StoreIcon style={ICON_STYLE} />, // 'Not Arrived'
3: <RecycleIcon style={ICON_STYLE} />, // 'Returned'
4: <AirplanemodeActiveIcon style={{ ...ICON_STYLE, color: 'green' }} />, // 'Shipped' - Active
5: <AirplanemodeActiveIcon style={{ ...ICON_STYLE, color: 'yellow' }} />, // 'Delayed'
};
// Define StepIconContainer to accept correct props and handle typing better
// Define StepIconContainer to accept correct props and handle typing better.
interface StepIconContainerProps extends React.HTMLAttributes<HTMLDivElement> {
color: string;
}
@ -32,70 +44,72 @@ type StepIconComponentProps = {
} & StepIconProps;
const StepIconComponent = ({ icon, dewar, ...props }: StepIconComponentProps) => {
const { iconIndex, color } = getIconProperties(icon, dewar);
const { iconIndex, color, fill } = getIconProperties(icon, dewar);
// Adjust icon color for the bottle especially since it's an SVG element
const IconComponent = ICONS[iconIndex];
const iconProps = iconIndex === 0 ? { fill: color } : {};
return (
<StepIconContainer color={color} {...props}>
{ICONS[iconIndex]}
{IconComponent
? React.cloneElement(IconComponent, iconProps)
: <Typography variant="body2" color="error">Invalid icon</Typography>
}
</StepIconContainer>
);
};
// Extracted function to determine icon properties
const getIconProperties = (icon: number, dewar: Dewar) => {
const iconIndex = icon - 1;
const color = determineIconColor(dewar, iconIndex);
return { iconIndex, color };
const status = dewar.status as DewarStatus;
const iconIndex = status === 'Delayed' && icon === 1 ? 5 : icon;
const color = determineIconColor(icon, status);
const fill = status === 'In Preparation' ? color : undefined;
return { iconIndex, color, fill };
};
// Original determineIconColor function remains unchanged
const determineIconColor = (dewar: Dewar, index: number) => {
let color = 'grey';
const STATUS_TO_STEP: Record<DewarStatus, number> = {
'In Preparation': 0,
'Ready for Shipping': 1,
'Shipped': 1,
'Delayed': 1,
'Not Arrived': 2,
'Arrived': 2,
'Returned': 3
};
if (index === 0) {
if (dewar.status === 'In Preparation') {
color = 'blue';
} else if (dewar.status === 'Ready for Shipping') {
color = 'green';
}
const getStatusStepIndex = (status: DewarStatus): number => STATUS_TO_STEP[status];
const determineIconColor = (iconIndex: number, status: DewarStatus): string => {
const statusIndex = getStatusStepIndex(status);
if (status === 'Delayed' && iconIndex === 1) {
return 'yellow';
}
if (index === 1) {
if (dewar.status === 'Ready for Shipping' && dewar.shippingStatus !== 'shipped') {
color = 'blue';
} else if (dewar.shippingStatus === 'shipped') {
color = 'green';
}
}
if (index === 2) {
if (dewar.shippingStatus === 'shipped' && dewar.arrivalStatus !== 'arrived') {
color = 'blue';
} else if (dewar.arrivalStatus === 'arrived') {
color = 'green';
}
}
return color;
return iconIndex <= statusIndex ? (iconIndex === statusIndex ? (status === 'Shipped' ? 'green' : 'blue') : 'green') : 'grey';
};
// Define your steps
const steps = ['In Preparation', 'Ready for Shipping', 'Arrived'];
const steps = ['In-House', 'Transit', 'At SLS', 'Returned'];
const CustomStepper = ({ dewar }: { dewar: Dewar }) => {
// Determine the current active step
const activeStep = steps.indexOf(dewar.status) !== -1 ? steps.indexOf(dewar.status) : 0;
const activeStep = getStatusStepIndex(dewar.status as DewarStatus);
return (
<Stepper alternativeLabel activeStep={activeStep}>
{steps.map((label, index) => (
<Step key={label}>
<StepLabel
StepIconComponent={(stepProps) => <StepIconComponent {...stepProps} icon={index + 1} dewar={dewar} />}
StepIconComponent={(stepProps) => <StepIconComponent {...stepProps} icon={index} dewar={dewar} />}
>
{label}
</StepLabel>
<Typography variant="body2">
{index === 0 ? dewar.ready_date :
index === 1 ? dewar.shipping_date :
index === 2 ? dewar.arrival_date : ''}
index === 2 ? dewar.arrival_date :
index === 3 ? dewar.returning_date : null}
</Typography>
</Step>
))}

View File

@ -1,25 +1,25 @@
import React from 'react';
import {Box, Typography, Button, Stack, TextField} from '@mui/material';
import { Box, Typography, Button, Stack, TextField } from '@mui/material';
import QRCode from 'react-qr-code';
import DeleteIcon from "@mui/icons-material/Delete";
import {Dewar, Shipment_Input, DefaultService} from "../../openapi";
import {SxProps} from "@mui/system";
import { Dewar, Shipment_Input, DefaultService } from "../../openapi";
import { SxProps } from "@mui/system";
import CustomStepper from "./DewarStepper";
import DewarDetails from './DewarDetails';
interface ShipmentDetailsProps {
isCreatingShipment: boolean;
selectedShipment: Shipment_Input;
selectedDewar: Dewar | null;
setSelectedDewar: React.Dispatch<React.SetStateAction<Dewar | null>>;
setSelectedShipment: React.Dispatch<React.SetStateAction<Shipment_Input>>;
sx?: SxProps;
}
const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
selectedShipment,
setSelectedDewar,
setSelectedShipment,
sx = {},
}) => {
const [localSelectedDewar, setLocalSelectedDewar] = React.useState<Dewar | null>(null);
@ -33,11 +33,9 @@ const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
const totalSamples = selectedShipment.dewars.reduce((acc, dewar) => acc + (dewar.number_of_samples || 0), 0);
const handleDewarSelection = (dewar: Dewar) => {
if (setSelectedDewar) {
const newSelection = localSelectedDewar?.id === dewar.id ? null : dewar;
setLocalSelectedDewar(newSelection);
setSelectedDewar(newSelection);
}
const newSelection = localSelectedDewar?.id === dewar.id ? null : dewar;
setLocalSelectedDewar(newSelection);
setSelectedDewar(newSelection);
};
const handleDeleteDewar = () => {
@ -45,41 +43,72 @@ const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
const confirmed = window.confirm('Are you sure you want to delete this dewar?');
if (confirmed) {
const updatedDewars = selectedShipment.dewars.filter(dewar => dewar.tracking_number !== localSelectedDewar.tracking_number);
console.log('Updated Dewars:', updatedDewars);
setSelectedShipment((prev) => ({ ...prev, dewars: updatedDewars }));
setLocalSelectedDewar(null);
}
}
};
const handleNewDewarChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const {name, value} = e.target;
const { name, value } = e.target;
setNewDewar((prev) => ({
...prev,
[name]: value,
}));
};
const addDewarToList = (currentDewars: Array<Dewar>, newDewar: Dewar): Array<Dewar> => {
return [...currentDewars, newDewar];
};
const updateDewarsState = (prev: Shipment_Input, createdDewar: Dewar): Shipment_Input => {
const updatedDewars = addDewarToList(prev.dewars, createdDewar);
return { ...prev, dewars: updatedDewars };
};
const handleAddDewar = async () => {
if (selectedShipment && newDewar.dewar_name) {
try {
const newDewarToPost: Dewar = {
...newDewar as Dewar,
dewar_name: newDewar.dewar_name.trim() || 'Unnamed Dewar',
number_of_pucks: newDewar.number_of_pucks ?? 0,
number_of_samples: newDewar.number_of_samples ?? 0,
return_address: selectedShipment.return_address,
contact_person: selectedShipment.contact_person,
status: 'In preparation',
shippingStatus: 'not shipped',
arrivalStatus: 'not arrived',
qrcode: newDewar.qrcode || 'N/A',
};
await DefaultService.createDewarDewarsPost(newDewarToPost);
// Create a new dewar
const createdDewar = await DefaultService.createDewarDewarsPost(newDewarToPost);
console.log('Created Dewar:', createdDewar);
// Check IDs before calling backend
console.log('Adding dewar to shipment:', {
shipment_id: selectedShipment.shipment_id,
dewar_id: createdDewar.id,
});
// Make an API call to associate the dewar with the shipment
const updatedShipment = await DefaultService.addDewarToShipmentShipmentsShipmentIdAddDewarPost(
selectedShipment.shipment_id,
createdDewar.id
);
if (updatedShipment) {
setSelectedShipment(updatedShipment);
} else {
throw new Error("Failed to update shipment with new dewar");
}
setIsAddingDewar(false);
setNewDewar({dewar_name: '', tracking_number: ''});
setNewDewar({ dewar_name: '', tracking_number: '' });
} catch (error) {
alert("Failed to add dewar. Please try again.");
console.error("Error adding dewar:", error);
alert("Failed to add dewar or update shipment. Please try again.");
console.error("Error adding dewar or updating shipment:", error);
}
} else {
alert('Please fill in the Dewar Name');
@ -87,19 +116,19 @@ const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
};
return (
<Box sx={{...sx, padding: 2, textAlign: 'left'}}>
<Box sx={{ ...sx, padding: 2, textAlign: 'left' }}>
{!localSelectedDewar && !isAddingDewar && (
<Button
variant="contained"
onClick={() => setIsAddingDewar(true)}
sx={{marginBottom: 2}}
sx={{ marginBottom: 2 }}
>
Add Dewar
</Button>
)}
{isAddingDewar && (
<Box sx={{marginBottom: 2, width: '20%'}}>
<Box sx={{ marginBottom: 2, width: '20%' }}>
<Typography variant="h6">Add New Dewar</Typography>
<TextField
label="Dewar Name"
@ -107,9 +136,9 @@ const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
value={newDewar.dewar_name}
onChange={handleNewDewarChange}
fullWidth
sx={{marginBottom: 2}}
sx={{ marginBottom: 2 }}
/>
<Button variant="contained" color="primary" onClick={handleAddDewar} sx={{marginRight: 2}}>
<Button variant="contained" color="primary" onClick={handleAddDewar} sx={{ marginRight: 2 }}>
Save Dewar
</Button>
<Button variant="outlined" color="secondary" onClick={() => setIsAddingDewar(false)}>
@ -129,7 +158,7 @@ const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
dewar={localSelectedDewar}
trackingNumber={localSelectedDewar.tracking_number || ''}
setTrackingNumber={(value) => {
setLocalSelectedDewar((prev) => prev ? {...prev, tracking_number: value} : prev);
setLocalSelectedDewar((prev) => prev ? { ...prev, tracking_number: value as string } : prev);
}}
contactPersons={selectedShipment.contact_person}
returnAddresses={selectedShipment.return_address}
@ -161,7 +190,7 @@ const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
}}
>
{dewar.qrcode ? (
<QRCode value={dewar.qrcode} size={70}/>
<QRCode value={dewar.qrcode} size={70} />
) : (
<Box
sx={{
@ -180,7 +209,7 @@ const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
)}
</Box>
<Box sx={{flexGrow: 1, marginRight: 0}}>
<Box sx={{ flexGrow: 1, marginRight: 0 }}>
<Typography variant="body1">{dewar.dewar_name}</Typography>
<Typography variant="body2">Number of Pucks: {dewar.number_of_pucks || 0}</Typography>
<Typography variant="body2">Number of Samples: {dewar.number_of_samples || 0}</Typography>
@ -207,7 +236,7 @@ const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
alignSelf: 'center',
}}
>
<DeleteIcon/>
<DeleteIcon />
</Button>
)}
</Box>

View File

@ -30,7 +30,7 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({
street: '',
city: '',
zipcode: '',
country: '',
country: ''
});
const [newShipment, setNewShipment] = React.useState<Shipment_Input>({
@ -54,7 +54,7 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({
try {
const c: ContactPerson[] = await DefaultService.getContactsContactsGet();
setContactPersons(c);
} catch (err) {
} catch {
setErrorMessage('Failed to load contact persons. Please try again later.');
}
};
@ -63,7 +63,7 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({
try {
const a: Address[] = await DefaultService.getReturnAddressesReturnAddressesGet();
setReturnAddresses(a);
} catch (err) {
} catch {
setErrorMessage('Failed to load return addresses. Please try again later.');
}
};
@ -72,7 +72,7 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({
try {
const p: Proposal[] = await DefaultService.getProposalsProposalsGet();
setProposals(p);
} catch (err) {
} catch {
setErrorMessage('Failed to load proposals. Please try again later.');
}
};
@ -155,6 +155,7 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({
number: newShipment.proposal_number
}] : [],
return_address: newShipment.return_address ? newShipment.return_address.map(address => ({
id: address.id,
street: address.street,
city: address.city,
zipcode: address.zipcode,
@ -168,7 +169,7 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({
await DefaultService.createShipmentShipmentsPost(payload);
setErrorMessage(null);
onCancel(); // close the form after saving
} catch (error) {
} catch {
setErrorMessage('Failed to save shipment. Please try again.');
}
};
@ -224,7 +225,7 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({
const c: ContactPerson = await DefaultService.createContactContactsPost(payload);
setContactPersons([...contactPersons, c]);
setErrorMessage(null);
} catch (err) {
} catch {
setErrorMessage('Failed to create a new contact person. Please try again later.');
}
@ -250,7 +251,7 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({
const a: Address = await DefaultService.createReturnAddressReturnAddressesPost(payload);
setReturnAddresses([...returnAddresses, a]);
setErrorMessage(null);
} catch (err) {
} catch {
setErrorMessage('Failed to create a new return address. Please try again later.');
}

View File

@ -33,6 +33,7 @@ const ShipmentView: React.FC<ShipmentViewProps> = () => {
selectedShipment={selectedShipment}
selectedDewar={selectedDewar}
setSelectedDewar={setSelectedDewar}
setSelectedShipment={setSelectedShipment}
/>
);
}

View File

@ -6,7 +6,7 @@
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
"forceConsistentCasingInFileNames": true,
},
"include": [
"src"