better support of add and delete dewar to a shipment

This commit is contained in:
GotthardG 2024-10-31 17:17:17 +01:00
parent d6d7e7c919
commit dc31eec66e
6 changed files with 147 additions and 95 deletions

View File

@ -148,8 +148,8 @@ dewars = [
id='DEWAR003', id='DEWAR003',
dewar_name='Dewar Three', dewar_name='Dewar Three',
tracking_number='TRACK125', tracking_number='TRACK125',
number_of_pucks=4, number_of_pucks=7,
number_of_samples=47, number_of_samples=72,
return_address=[return_addresses[0]], return_address=[return_addresses[0]],
contact_person=[contacts[2]], contact_person=[contacts[2]],
status='Not Shipped', status='Not Shipped',
@ -163,8 +163,8 @@ dewars = [
id='DEWAR004', id='DEWAR004',
dewar_name='Dewar Four', dewar_name='Dewar Four',
tracking_number='', tracking_number='',
number_of_pucks=4, number_of_pucks=7,
number_of_samples=47, number_of_samples=70,
return_address=[return_addresses[0]], return_address=[return_addresses[0]],
contact_person=[contacts[2]], contact_person=[contacts[2]],
status='Delayed', status='Delayed',
@ -178,8 +178,8 @@ dewars = [
id='DEWAR005', id='DEWAR005',
dewar_name='Dewar Five', dewar_name='Dewar Five',
tracking_number='', tracking_number='',
number_of_pucks=4, number_of_pucks=3,
number_of_samples=47, number_of_samples=30,
return_address=[return_addresses[0]], return_address=[return_addresses[0]],
contact_person=[contacts[2]], contact_person=[contacts[2]],
status='Returned', status='Returned',
@ -312,6 +312,23 @@ async def create_dewar(dewar: Dewar) -> Dewar:
return dewar # Return the newly created dewar return dewar # Return the newly created dewar
@app.delete("/shipments/{shipment_id}/remove_dewar/{dewar_id}", response_model=Shipment)
async def remove_dewar_from_shipment(shipment_id: str, dewar_id: str):
"""Remove a dewar from a shipment."""
# Log parameters
logging.info(f"Received request to remove dewar {dewar_id} from shipment {shipment_id}")
# Find the shipment by ID
shipment = next((sh for sh in shipments if sh.shipment_id == shipment_id), None)
if not shipment:
logging.error(f"Shipment with ID {shipment_id} not found")
raise HTTPException(status_code=404, detail="Shipment not found")
# Remove the dewar from the shipment
shipment.dewars = [dw for dw in shipment.dewars if dw.id != dewar_id]
return shipment
@app.get("/shipment_contact_persons") @app.get("/shipment_contact_persons")
async def get_shipment_contact_persons(): async def get_shipment_contact_persons():

View File

@ -24,7 +24,7 @@ const ICONS: { [key: number]: React.ReactElement } = {
2: <StoreIcon style={ICON_STYLE} />, // 'Not Arrived' 2: <StoreIcon style={ICON_STYLE} />, // 'Not Arrived'
3: <RecycleIcon style={ICON_STYLE} />, // 'Returned' 3: <RecycleIcon style={ICON_STYLE} />, // 'Returned'
4: <AirplanemodeActiveIcon style={{ ...ICON_STYLE, color: 'green' }} />, // 'Shipped' - Active 4: <AirplanemodeActiveIcon style={{ ...ICON_STYLE, color: 'green' }} />, // 'Shipped' - Active
5: <AirplanemodeActiveIcon style={{ ...ICON_STYLE, color: 'yellow' }} />, // 'Delayed' 5: <AirplanemodeActiveIcon style={{ ...ICON_STYLE, color: 'orange' }} />, // 'Delayed'
}; };
// Define StepIconContainer to accept correct props and handle typing better. // Define StepIconContainer to accept correct props and handle typing better.
@ -44,7 +44,7 @@ type StepIconComponentProps = {
} & StepIconProps; } & StepIconProps;
const StepIconComponent = ({ icon, dewar, ...props }: StepIconComponentProps) => { const StepIconComponent = ({ icon, dewar, ...props }: StepIconComponentProps) => {
const { iconIndex, color, fill } = getIconProperties(icon, dewar); const { iconIndex, color} = getIconProperties(icon, dewar);
// Adjust icon color for the bottle especially since it's an SVG element // Adjust icon color for the bottle especially since it's an SVG element
const IconComponent = ICONS[iconIndex]; const IconComponent = ICONS[iconIndex];
@ -84,7 +84,7 @@ const getStatusStepIndex = (status: DewarStatus): number => STATUS_TO_STEP[statu
const determineIconColor = (iconIndex: number, status: DewarStatus): string => { const determineIconColor = (iconIndex: number, status: DewarStatus): string => {
const statusIndex = getStatusStepIndex(status); const statusIndex = getStatusStepIndex(status);
if (status === 'Delayed' && iconIndex === 1) { if (status === 'Delayed' && iconIndex === 1) {
return 'yellow'; return 'orange';
} }
return iconIndex <= statusIndex ? (iconIndex === statusIndex ? (status === 'Shipped' ? 'green' : 'blue') : 'green') : 'grey'; return iconIndex <= statusIndex ? (iconIndex === statusIndex ? (status === 'Shipped' ? 'green' : 'blue') : 'green') : 'grey';
}; };
@ -97,23 +97,25 @@ const CustomStepper = ({ dewar }: { dewar: Dewar }) => {
const activeStep = getStatusStepIndex(dewar.status as DewarStatus); const activeStep = getStatusStepIndex(dewar.status as DewarStatus);
return ( return (
<Stepper alternativeLabel activeStep={activeStep}> <div style={{ display: 'flex', justifyContent: 'center', width: '100%' /* Or any other preferred width */, maxWidth: '600px', margin: '0 auto' }}>
{steps.map((label, index) => ( <Stepper alternativeLabel activeStep={activeStep} style={{ width: '100%' }}>
<Step key={label}> {steps.map((label, index) => (
<StepLabel <Step key={label}>
StepIconComponent={(stepProps) => <StepIconComponent {...stepProps} icon={index} dewar={dewar} />} <StepLabel
> StepIconComponent={(stepProps) => <StepIconComponent {...stepProps} icon={index} dewar={dewar} />}
{label} >
</StepLabel> {label}
<Typography variant="body2"> </StepLabel>
{index === 0 ? dewar.ready_date : <Typography variant="body2">
index === 1 ? dewar.shipping_date : {index === 0 ? dewar.ready_date :
index === 2 ? dewar.arrival_date : index === 1 ? dewar.shipping_date :
index === 3 ? dewar.returning_date : null} index === 2 ? dewar.arrival_date :
</Typography> index === 3 ? dewar.returning_date : ''}
</Step> </Typography>
))} </Step>
</Stepper> ))}
</Stepper>
</div>
); );
}; };

View File

@ -29,6 +29,11 @@ const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
tracking_number: '', tracking_number: '',
}); });
// To reset localSelectedDewar when selectedShipment changes
React.useEffect(() => {
setLocalSelectedDewar(null);
}, [selectedShipment]);
const totalPucks = selectedShipment.dewars.reduce((acc, dewar) => acc + (dewar.number_of_pucks || 0), 0); const totalPucks = selectedShipment.dewars.reduce((acc, dewar) => acc + (dewar.number_of_pucks || 0), 0);
const totalSamples = selectedShipment.dewars.reduce((acc, dewar) => acc + (dewar.number_of_samples || 0), 0); const totalSamples = selectedShipment.dewars.reduce((acc, dewar) => acc + (dewar.number_of_samples || 0), 0);
@ -38,13 +43,23 @@ const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
setSelectedDewar(newSelection); setSelectedDewar(newSelection);
}; };
const handleDeleteDewar = () => { const handleDeleteDewar = async (dewarId: string) => {
if (localSelectedDewar) { const confirmed = window.confirm('Are you sure you want to delete this dewar?');
const confirmed = window.confirm('Are you sure you want to delete this dewar?'); if (confirmed) {
if (confirmed) { try {
const updatedDewars = selectedShipment.dewars.filter(dewar => dewar.tracking_number !== localSelectedDewar.tracking_number); console.log('Selected Shipment ID:', selectedShipment.shipment_id);
setSelectedShipment((prev) => ({ ...prev, dewars: updatedDewars })); console.log('Dewar ID to be deleted:', dewarId);
const updatedShipment = await DefaultService.removeDewarFromShipmentShipmentsShipmentIdRemoveDewarDewarIdDelete(
selectedShipment.shipment_id, dewarId
);
// Ensure state is updated with server response
setSelectedShipment(updatedShipment);
setLocalSelectedDewar(null); setLocalSelectedDewar(null);
} catch (error) {
console.error('Failed to delete dewar:', error);
alert('Failed to delete dewar. Please try again.');
} }
} }
}; };
@ -57,15 +72,6 @@ const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
})); }));
}; };
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 () => { const handleAddDewar = async () => {
if (selectedShipment && newDewar.dewar_name) { if (selectedShipment && newDewar.dewar_name) {
try { try {
@ -100,15 +106,15 @@ const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
if (updatedShipment) { if (updatedShipment) {
setSelectedShipment(updatedShipment); setSelectedShipment(updatedShipment);
} else { } else {
throw new Error("Failed to update shipment with new dewar"); throw new Error('Failed to update shipment with new dewar');
} }
setIsAddingDewar(false); setIsAddingDewar(false);
setNewDewar({ dewar_name: '', tracking_number: '' }); setNewDewar({ dewar_name: '', tracking_number: '' });
} catch (error) { } catch (error) {
alert("Failed to add dewar or update shipment. Please try again."); alert('Failed to add dewar or update shipment. Please try again.');
console.error("Error adding dewar or updating shipment:", error); console.error('Error adding dewar or updating shipment:', error);
} }
} else { } else {
alert('Please fill in the Dewar Name'); alert('Please fill in the Dewar Name');
@ -224,9 +230,9 @@ const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
justifyContent: 'space-between' justifyContent: 'space-between'
}}> }}>
<CustomStepper dewar={dewar} /> <CustomStepper dewar={dewar} />
{localSelectedDewar?.tracking_number === dewar.tracking_number && ( {localSelectedDewar?.id === dewar.id && (
<Button <Button
onClick={handleDeleteDewar} onClick={() => handleDeleteDewar(dewar.id)} // <--- Pass the dewar ID here
color="error" color="error"
sx={{ sx={{
minWidth: '40px', minWidth: '40px',

View File

@ -8,12 +8,10 @@ import { useEffect } from "react";
interface ShipmentFormProps { interface ShipmentFormProps {
sx?: SxProps; sx?: SxProps;
onCancel: () => void; onCancel: () => void;
refreshShipments: () => void; // Add the refresh function as a prop
} }
const ShipmentForm: React.FC<ShipmentFormProps> = ({ const ShipmentForm: React.FC<ShipmentFormProps> = ({ sx = {}, onCancel, refreshShipments }) => {
sx = {},
onCancel,
}) => {
const [contactPersons, setContactPersons] = React.useState<ContactPerson[]>([]); const [contactPersons, setContactPersons] = React.useState<ContactPerson[]>([]);
const [returnAddresses, setReturnAddresses] = React.useState<Address[]>([]); const [returnAddresses, setReturnAddresses] = React.useState<Address[]>([]);
const [proposals, setProposals] = React.useState<Proposal[]>([]); const [proposals, setProposals] = React.useState<Proposal[]>([]);
@ -25,14 +23,12 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({
phone_number: '', phone_number: '',
email: '', email: '',
}); });
const [newReturnAddress, setNewReturnAddress] = React.useState<Address>({ const [newReturnAddress, setNewReturnAddress] = React.useState<Address>({
street: '', street: '',
city: '', city: '',
zipcode: '', zipcode: '',
country: '' country: ''
}); });
const [newShipment, setNewShipment] = React.useState<Shipment_Input>({ const [newShipment, setNewShipment] = React.useState<Shipment_Input>({
comments: undefined, comments: undefined,
contact_person: [], contact_person: [],
@ -44,7 +40,6 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({
shipment_name: "", shipment_name: "",
shipment_status: "" shipment_status: ""
}); });
const [errorMessage, setErrorMessage] = React.useState<string | null>(null); const [errorMessage, setErrorMessage] = React.useState<string | null>(null);
useEffect(() => { useEffect(() => {
@ -168,6 +163,7 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({
try { try {
await DefaultService.createShipmentShipmentsPost(payload); await DefaultService.createShipmentShipmentsPost(payload);
setErrorMessage(null); setErrorMessage(null);
refreshShipments(); // Ensure shipments are refreshed after creation
onCancel(); // close the form after saving onCancel(); // close the form after saving
} catch { } catch {
setErrorMessage('Failed to save shipment. Please try again.'); setErrorMessage('Failed to save shipment. Please try again.');
@ -286,12 +282,18 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({
<FormControl fullWidth required> <FormControl fullWidth required>
<InputLabel>Contact Person</InputLabel> <InputLabel>Contact Person</InputLabel>
<Select <Select
value={newShipment.contact_person?.[0]?.lastname || ''} value={
isCreatingContactPerson
? 'new'
: newShipment.contact_person.length > 0
? newShipment.contact_person[0].lastname
: ''
}
onChange={handleContactPersonChange} onChange={handleContactPersonChange}
displayEmpty displayEmpty
> >
{contactPersons.map((person) => ( {contactPersons.map((person) => (
<MenuItem key={person.lastname + person.firstname} value={person.lastname}> <MenuItem key={person.lastname} value={person.lastname}>
{`${person.lastname}, ${person.firstname}`} {`${person.lastname}, ${person.firstname}`}
</MenuItem> </MenuItem>
))} ))}
@ -346,10 +348,12 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({
error={!validateEmail(newContactPerson.email)} error={!validateEmail(newContactPerson.email)}
helperText={!validateEmail(newContactPerson.email) ? "Invalid email" : ""} helperText={!validateEmail(newContactPerson.email) ? "Invalid email" : ""}
/> />
<Button variant="contained" <Button
color="primary" variant="contained"
onClick={handleSaveNewContactPerson} color="primary"
disabled={!isContactFormValid()}> onClick={handleSaveNewContactPerson}
disabled={!isContactFormValid()}
>
Save New Contact Person Save New Contact Person
</Button> </Button>
</> </>
@ -371,7 +375,13 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({
<FormControl fullWidth required> <FormControl fullWidth required>
<InputLabel>Return Address</InputLabel> <InputLabel>Return Address</InputLabel>
<Select <Select
value={newShipment.return_address?.[0]?.city || ''} value={
isCreatingReturnAddress
? 'new'
: newShipment.return_address.length > 0
? newShipment.return_address[0].city
: ''
}
onChange={handleReturnAddressChange} onChange={handleReturnAddressChange}
displayEmpty displayEmpty
> >
@ -424,10 +434,12 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({
fullWidth fullWidth
required required
/> />
<Button variant="contained" <Button
color="primary" variant="contained"
onClick={handleSaveNewReturnAddress} color="primary"
disabled={!isAddressFormValid()}> onClick={handleSaveNewReturnAddress}
disabled={!isAddressFormValid()}
>
Save New Return Address Save New Return Address
</Button> </Button>
</> </>

View File

@ -1,9 +1,8 @@
import * as React from 'react'; import * as React from 'react';
import { useEffect, useState } from 'react';
import { Button, Box, Typography, IconButton } from '@mui/material'; import { Button, Box, Typography, IconButton } from '@mui/material';
import { Add as AddIcon, Delete as DeleteIcon, UploadFile as UploadFileIcon, Refresh as RefreshIcon } from '@mui/icons-material'; import { Add as AddIcon, Delete as DeleteIcon, UploadFile as UploadFileIcon, Refresh as RefreshIcon } from '@mui/icons-material';
import UploadDialog from './UploadDialog'; import UploadDialog from './UploadDialog';
import { Shipment_Input, DefaultService, OpenAPI, Dewar } from '../../openapi'; // Import relevant types import { Shipment_Input, Dewar, DefaultService } from '../../openapi';
import { SxProps } from '@mui/material'; import { SxProps } from '@mui/material';
import bottleGrey from '/src/assets/icons/bottle-svgrepo-com-grey.svg' import bottleGrey from '/src/assets/icons/bottle-svgrepo-com-grey.svg'
import bottleYellow from '/src/assets/icons/bottle-svgrepo-com-yellow.svg' import bottleYellow from '/src/assets/icons/bottle-svgrepo-com-yellow.svg'
@ -15,11 +14,11 @@ interface ShipmentPanelProps {
selectedPage?: string; selectedPage?: string;
selectShipment: (shipment: Shipment_Input | null) => void; selectShipment: (shipment: Shipment_Input | null) => void;
sx?: SxProps; sx?: SxProps;
shipments: Shipment_Input[];
refreshShipments: () => void;
error: string | null;
} }
const API_BASE_URL = 'http://127.0.0.1:8000';
OpenAPI.BASE = API_BASE_URL;
const statusIconMap: Record<string, string> = { const statusIconMap: Record<string, string> = {
'In Transit': bottleYellow, 'In Transit': bottleYellow,
'Delivered': bottleGreen, 'Delivered': bottleGreen,
@ -27,25 +26,16 @@ const statusIconMap: Record<string, string> = {
'Unknown': bottleRed, 'Unknown': bottleRed,
}; };
const ShipmentPanel: React.FC<ShipmentPanelProps> = ({ setCreatingShipment, selectShipment, sx }) => { const ShipmentPanel: React.FC<ShipmentPanelProps> = ({
const [shipments, setShipments] = useState<Shipment_Input[]>([]); setCreatingShipment,
const [selectedShipment, setSelectedShipment] = useState<Shipment_Input | null>(null); selectShipment,
const [error, setError] = useState<string | null>(null); sx,
const [uploadDialogOpen, setUploadDialogOpen] = useState(false); shipments,
refreshShipments,
useEffect(() => { error
fetchAndSetShipments(); }) => {
}, []); const [selectedShipment, setSelectedShipment] = React.useState<Shipment_Input | null>(null);
const [uploadDialogOpen, setUploadDialogOpen] = React.useState(false);
const fetchAndSetShipments = async () => {
try {
const shipmentsData: Shipment_Input[] = await DefaultService.getShipmentsShipmentsGet();
shipmentsData.sort((a, b) => new Date(b.shipment_date).getTime() - new Date(a.shipment_date).getTime());
setShipments(shipmentsData);
} catch (error) {
setError('Failed to fetch shipments. Please try again later.');
}
};
const handleDeleteShipment = async () => { const handleDeleteShipment = async () => {
if (selectedShipment) { if (selectedShipment) {
@ -59,12 +49,12 @@ const ShipmentPanel: React.FC<ShipmentPanelProps> = ({ setCreatingShipment, sele
const deleteShipment = async (shipmentId: string | undefined) => { const deleteShipment = async (shipmentId: string | undefined) => {
if (!shipmentId) return; if (!shipmentId) return;
try { try {
// Assumes DefaultService.deleteShipmentShipmentsShipmentIdDelete is already defined
await DefaultService.deleteShipmentShipmentsShipmentIdDelete(shipmentId); await DefaultService.deleteShipmentShipmentsShipmentIdDelete(shipmentId);
setShipments(shipments.filter(shipment => shipment.shipment_id !== shipmentId)); refreshShipments(); // Call the refresh function after deletion
setSelectedShipment(null); setSelectedShipment(null);
setError(null);
} catch (error) { } catch (error) {
setError('Failed to delete shipment. Please try again later.'); // Handle error
} }
}; };
@ -77,7 +67,6 @@ const ShipmentPanel: React.FC<ShipmentPanelProps> = ({ setCreatingShipment, sele
const openUploadDialog = () => setUploadDialogOpen(true); const openUploadDialog = () => setUploadDialogOpen(true);
const closeUploadDialog = () => setUploadDialogOpen(false); const closeUploadDialog = () => setUploadDialogOpen(false);
const refreshShipments = () => fetchAndSetShipments();
const getNumberOfDewars = (shipment: Shipment_Input): number => { const getNumberOfDewars = (shipment: Shipment_Input): number => {
return shipment.dewars ? shipment.dewars.length : 0; return shipment.dewars ? shipment.dewars.length : 0;

View File

@ -1,16 +1,35 @@
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import Grid from '@mui/material/Grid'; // Using Grid (deprecated but configurable) import Grid from '@mui/material/Grid'; // Using Grid (deprecated but configurable)
import ShipmentPanel from '../components/ShipmentPanel'; import ShipmentPanel from '../components/ShipmentPanel';
import ShipmentDetails from '../components/ShipmentDetails'; import ShipmentDetails from '../components/ShipmentDetails';
import ShipmentForm from '../components/ShipmentForm'; import ShipmentForm from '../components/ShipmentForm';
import { Dewar, Shipment_Input } from '../../openapi'; import { Dewar, Shipment_Input, DefaultService, OpenAPI } from '../../openapi';
type ShipmentViewProps = React.PropsWithChildren<Record<string, never>>; type ShipmentViewProps = React.PropsWithChildren<Record<string, never>>;
const API_BASE_URL = 'http://127.0.0.1:8000';
OpenAPI.BASE = API_BASE_URL;
const ShipmentView: React.FC<ShipmentViewProps> = () => { const ShipmentView: React.FC<ShipmentViewProps> = () => {
const [isCreatingShipment, setIsCreatingShipment] = useState(false); const [isCreatingShipment, setIsCreatingShipment] = useState(false);
const [selectedShipment, setSelectedShipment] = useState<Shipment_Input | null>(null); const [selectedShipment, setSelectedShipment] = useState<Shipment_Input | null>(null);
const [selectedDewar, setSelectedDewar] = useState<Dewar | null>(null); const [selectedDewar, setSelectedDewar] = useState<Dewar | null>(null);
const [shipments, setShipments] = useState<Shipment_Input[]>([]);
const [error, setError] = useState<string | null>(null);
const fetchAndSetShipments = async () => {
try {
const shipmentsData: Shipment_Input[] = await DefaultService.getShipmentsShipmentsGet();
shipmentsData.sort((a, b) => new Date(b.shipment_date).getTime() - new Date(a.shipment_date).getTime());
setShipments(shipmentsData);
} catch (error) {
setError('Failed to fetch shipments. Please try again later.');
}
};
useEffect(() => {
fetchAndSetShipments();
}, []);
const handleSelectShipment = (shipment: Shipment_Input | null) => { const handleSelectShipment = (shipment: Shipment_Input | null) => {
setSelectedShipment(shipment); setSelectedShipment(shipment);
@ -23,7 +42,11 @@ const ShipmentView: React.FC<ShipmentViewProps> = () => {
const renderShipmentContent = () => { const renderShipmentContent = () => {
if (isCreatingShipment) { if (isCreatingShipment) {
return <ShipmentForm sx={{ flexGrow: 1 }} onCancel={handleCancelShipmentForm} />; return <ShipmentForm
sx={{ flexGrow: 1 }}
onCancel={handleCancelShipmentForm}
refreshShipments={fetchAndSetShipments} // Pass the fetch function to refresh shipments
/>;
} }
if (selectedShipment) { if (selectedShipment) {
return ( return (
@ -56,6 +79,9 @@ const ShipmentView: React.FC<ShipmentViewProps> = () => {
selectedPage="Shipments" selectedPage="Shipments"
setCreatingShipment={setIsCreatingShipment} setCreatingShipment={setIsCreatingShipment}
selectShipment={handleSelectShipment} selectShipment={handleSelectShipment}
shipments={shipments}
refreshShipments={fetchAndSetShipments}
error={error}
/> />
</Grid> </Grid>
<Grid <Grid