355 lines
17 KiB
TypeScript
355 lines
17 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { Box, Typography, Button, Stack, TextField, IconButton, Grid } from '@mui/material';
|
|
import QRCode from 'react-qr-code';
|
|
import DeleteIcon from "@mui/icons-material/Delete";
|
|
import CheckIcon from '@mui/icons-material/Check';
|
|
import CloseIcon from '@mui/icons-material/Close';
|
|
import { Dewar, DewarsService, Shipment, ContactPerson, ApiError, ShipmentsService } from "../../openapi";
|
|
import { SxProps } from "@mui/system";
|
|
import CustomStepper from "./DewarStepper";
|
|
import DewarDetails from './DewarDetails';
|
|
|
|
const MAX_COMMENTS_LENGTH = 200;
|
|
|
|
interface ShipmentDetailsProps {
|
|
isCreatingShipment: boolean;
|
|
sx?: SxProps;
|
|
selectedShipment: Shipment | null;
|
|
selectedDewar: Dewar | null;
|
|
setSelectedDewar: React.Dispatch<React.SetStateAction<Dewar | null>>;
|
|
setSelectedShipment: React.Dispatch<React.SetStateAction<Shipment | null>>;
|
|
refreshShipments: () => void;
|
|
defaultContactPerson?: ContactPerson;
|
|
}
|
|
|
|
const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
|
|
sx,
|
|
selectedShipment,
|
|
setSelectedDewar,
|
|
setSelectedShipment,
|
|
refreshShipments,
|
|
}) => {
|
|
const [localSelectedDewar, setLocalSelectedDewar] = useState<Dewar | null>(null);
|
|
const [isAddingDewar, setIsAddingDewar] = useState<boolean>(false);
|
|
const [comments, setComments] = useState<string>(selectedShipment?.comments || '');
|
|
const [initialComments, setInitialComments] = useState<string>(selectedShipment?.comments || '');
|
|
|
|
const initialNewDewarState: Partial<Dewar> = {
|
|
dewar_name: '',
|
|
tracking_number: '',
|
|
number_of_pucks: 0,
|
|
number_of_samples: 0,
|
|
status: 'In preparation',
|
|
ready_date: null,
|
|
shipping_date: null,
|
|
arrival_date: null,
|
|
returning_date: null,
|
|
contact_person_id: selectedShipment?.contact_person?.id,
|
|
return_address_id: selectedShipment?.return_address?.id,
|
|
};
|
|
|
|
const [newDewar, setNewDewar] = useState<Partial<Dewar>>(initialNewDewarState);
|
|
|
|
useEffect(() => {
|
|
setLocalSelectedDewar(null);
|
|
// Ensure to update the default contact person and return address when the shipment changes
|
|
setNewDewar((prev) => ({
|
|
...prev,
|
|
contact_person_id: selectedShipment?.contact_person?.id,
|
|
return_address_id: selectedShipment?.return_address?.id
|
|
}));
|
|
}, [selectedShipment]);
|
|
|
|
useEffect(() => {
|
|
setComments(selectedShipment?.comments || '');
|
|
setInitialComments(selectedShipment?.comments || '');
|
|
}, [selectedShipment]);
|
|
|
|
const totalPucks = selectedShipment?.dewars?.reduce((acc, dewar) => acc + (dewar.number_of_pucks || 0), 0) || 0;
|
|
const totalSamples = selectedShipment?.dewars?.reduce((acc, dewar) => acc + (dewar.number_of_samples || 0), 0) || 0;
|
|
|
|
const handleDewarSelection = (dewar: Dewar) => {
|
|
const newSelection = localSelectedDewar?.id === dewar.id ? null : dewar;
|
|
setLocalSelectedDewar(newSelection);
|
|
setSelectedDewar(newSelection);
|
|
};
|
|
|
|
const handleDeleteDewar = async (dewarId: number) => {
|
|
const confirmed = window.confirm('Are you sure you want to delete this dewar?');
|
|
if (confirmed && selectedShipment) {
|
|
try {
|
|
const updatedShipment = await ShipmentsService.removeDewarFromShipmentShipmentsShipmentIdRemoveDewarDewarIdDelete(selectedShipment.id, dewarId);
|
|
setSelectedShipment(updatedShipment);
|
|
setLocalSelectedDewar(null);
|
|
refreshShipments();
|
|
} catch (error) {
|
|
console.error('Failed to delete dewar:', error);
|
|
alert('Failed to delete dewar. Please try again.');
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleNewDewarChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const { name, value } = e.target;
|
|
setNewDewar((prev) => ({
|
|
...prev,
|
|
[name]: value,
|
|
}));
|
|
};
|
|
|
|
const handleAddDewar = async () => {
|
|
if (newDewar.dewar_name?.trim()) {
|
|
try {
|
|
const newDewarToPost: Dewar = {
|
|
...initialNewDewarState,
|
|
...newDewar,
|
|
dewar_name: newDewar.dewar_name.trim(),
|
|
contact_person_id: selectedShipment?.contact_person?.id,
|
|
return_address_id: selectedShipment?.return_address?.id
|
|
} as Dewar;
|
|
|
|
if (!newDewarToPost.dewar_name || !newDewarToPost.status) {
|
|
throw new Error('Missing required fields');
|
|
}
|
|
|
|
const createdDewar = await DewarsService.createDewarDewarsPost(newDewarToPost);
|
|
|
|
if (createdDewar && selectedShipment) {
|
|
const updatedShipment = await ShipmentsService.addDewarToShipmentShipmentsShipmentIdAddDewarPost(selectedShipment.id, createdDewar.id);
|
|
setSelectedShipment(updatedShipment);
|
|
setIsAddingDewar(false);
|
|
setNewDewar(initialNewDewarState);
|
|
refreshShipments();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error adding dewar or updating shipment:', error);
|
|
if (error instanceof ApiError && error.body) {
|
|
console.error('Validation errors:', error.body.detail);
|
|
} else {
|
|
console.error('Unexpected error:', error);
|
|
}
|
|
alert('Failed to add dewar or update shipment. Please check the data and try again.');
|
|
}
|
|
} else {
|
|
alert('Please fill in the Dewar Name');
|
|
}
|
|
};
|
|
|
|
const handleSaveComments = async () => {
|
|
if (selectedShipment && selectedShipment.id) {
|
|
try {
|
|
const payload = { comments };
|
|
|
|
// Assuming `updateShipmentCommentsShipmentsShipmentIdCommentsPut` only needs the shipment ID
|
|
const updatedShipment = await ShipmentsService.updateShipmentCommentsShipmentsShipmentIdCommentsPut(selectedShipment.id, payload);
|
|
|
|
setSelectedShipment({ ...selectedShipment, comments: updatedShipment.comments });
|
|
setInitialComments(comments);
|
|
refreshShipments();
|
|
alert('Comments updated successfully.');
|
|
} catch (error) {
|
|
console.error('Failed to update comments:', error);
|
|
alert('Failed to update comments. Please try again.');
|
|
}
|
|
} else {
|
|
console.error("Selected shipment or shipment ID is undefined");
|
|
}
|
|
};
|
|
|
|
const handleCancelEdit = () => {
|
|
setComments(initialComments);
|
|
};
|
|
|
|
const isCommentsEdited = comments !== initialComments;
|
|
const contactPerson = selectedShipment?.contact_person;
|
|
|
|
return (
|
|
<Box sx={{ ...sx, padding: 2, textAlign: 'left' }}>
|
|
{!localSelectedDewar && !isAddingDewar && (
|
|
<Button
|
|
variant="contained"
|
|
onClick={() => setIsAddingDewar(true)}
|
|
sx={{ marginBottom: 2 }}
|
|
>
|
|
Add Dewar
|
|
</Button>
|
|
)}
|
|
|
|
{isAddingDewar && (
|
|
<Box sx={{ marginBottom: 2, width: '20%' }}>
|
|
<Typography variant="h6">Add New Dewar</Typography>
|
|
<TextField
|
|
label="Dewar Name"
|
|
name="dewar_name"
|
|
value={newDewar.dewar_name}
|
|
onChange={handleNewDewarChange}
|
|
fullWidth
|
|
sx={{ marginBottom: 2 }}
|
|
/>
|
|
<Button
|
|
variant="contained"
|
|
color="primary"
|
|
onClick={handleAddDewar}
|
|
sx={{ marginRight: 2 }}
|
|
>
|
|
Save Dewar
|
|
</Button>
|
|
<Button
|
|
variant="outlined"
|
|
color="secondary"
|
|
onClick={() => setIsAddingDewar(false)}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
</Box>
|
|
)}
|
|
|
|
{selectedShipment ? (
|
|
<Grid container spacing={2}>
|
|
<Grid item xs={12} md={6}>
|
|
<Box sx={{ marginTop: 2, marginBottom: 2 }}>
|
|
<Typography variant="h5">{selectedShipment.shipment_name}</Typography>
|
|
<Typography variant="body1" color="textSecondary">
|
|
Main contact person: {contactPerson ? `${contactPerson.firstname} ${contactPerson.lastname}` : 'N/A'}
|
|
</Typography>
|
|
<Typography variant="body1">Number of Pucks: {totalPucks}</Typography>
|
|
<Typography variant="body1">Number of Samples: {totalSamples}</Typography>
|
|
<Typography variant="body1">Shipment Date: {selectedShipment.shipment_date}</Typography>
|
|
</Box>
|
|
</Grid>
|
|
<Grid item xs={12} md={6}>
|
|
<Box sx={{ position: 'relative' }}>
|
|
<TextField
|
|
label="Comments"
|
|
fullWidth
|
|
multiline
|
|
rows={4}
|
|
value={comments}
|
|
onChange={(e) => setComments(e.target.value)}
|
|
sx={{
|
|
marginBottom: 2,
|
|
'& .MuiInputBase-root': {
|
|
color: isCommentsEdited ? 'inherit' : 'rgba(0, 0, 0, 0.6)',
|
|
},
|
|
}}
|
|
helperText={`${MAX_COMMENTS_LENGTH - comments.length} characters remaining`}
|
|
error={comments.length > MAX_COMMENTS_LENGTH}
|
|
/>
|
|
<Box sx={{ position: 'absolute', bottom: 8, right: 8, display: 'flex', gap: 1 }}>
|
|
<IconButton
|
|
color="primary"
|
|
onClick={handleSaveComments}
|
|
disabled={comments.length > MAX_COMMENTS_LENGTH}
|
|
>
|
|
<CheckIcon />
|
|
</IconButton>
|
|
<IconButton color="secondary" onClick={handleCancelEdit}>
|
|
<CloseIcon />
|
|
</IconButton>
|
|
</Box>
|
|
</Box>
|
|
</Grid>
|
|
</Grid>
|
|
) : (
|
|
<Typography variant="h5" color="error">No shipment selected</Typography>
|
|
)}
|
|
|
|
<Stack spacing={1}>
|
|
{selectedShipment?.dewars?.map((dewar) => (
|
|
<React.Fragment key={dewar.id}>
|
|
<Button
|
|
onClick={() => handleDewarSelection(dewar)}
|
|
variant="outlined"
|
|
sx={{
|
|
display: 'flex',
|
|
justifyContent: 'flex-start',
|
|
alignItems: 'center',
|
|
padding: localSelectedDewar?.id === dewar.id ? 2 : 1,
|
|
textTransform: 'none',
|
|
width: '100%',
|
|
backgroundColor: localSelectedDewar?.id === dewar.id ? '#f0f0f0' : '#fff',
|
|
transition: 'all 0.3s',
|
|
overflow: 'hidden',
|
|
border: localSelectedDewar?.id === dewar.id ? '2px solid #000' : undefined,
|
|
}}
|
|
>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', marginRight: 2 }}>
|
|
{dewar.unique_id ? (
|
|
<QRCode value={dewar.unique_id} size={70} />
|
|
) : (
|
|
<Box
|
|
sx={{
|
|
width: 70,
|
|
height: 70,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
border: '1px dashed #ccc',
|
|
borderRadius: 1,
|
|
color: 'text.secondary'
|
|
}}
|
|
>
|
|
<Typography variant="body2">No QR Code</Typography>
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
|
|
<Box sx={{ flexGrow: 1 }}>
|
|
<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>
|
|
<Typography variant="body2">Tracking Number: {dewar.tracking_number}</Typography>
|
|
<Typography variant="body2">
|
|
Contact Person: {dewar.contact_person?.firstname ? `${dewar.contact_person.firstname} ${dewar.contact_person.lastname}` : 'N/A'}
|
|
</Typography>
|
|
</Box>
|
|
<Box sx={{
|
|
flexGrow: 1,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between'
|
|
}}>
|
|
<CustomStepper dewar={dewar} selectedDewarId={localSelectedDewar?.id ?? null} refreshShipments={refreshShipments} />
|
|
</Box>
|
|
|
|
{localSelectedDewar?.id === dewar.id && (
|
|
<IconButton
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
handleDeleteDewar(dewar.id);
|
|
}}
|
|
color="error"
|
|
sx={{
|
|
minWidth: '40px',
|
|
height: '40px',
|
|
}}
|
|
>
|
|
<DeleteIcon />
|
|
</IconButton>
|
|
)}
|
|
</Button>
|
|
|
|
{localSelectedDewar?.id === dewar.id && (
|
|
<DewarDetails
|
|
dewar={localSelectedDewar}
|
|
trackingNumber={localSelectedDewar?.tracking_number || ''}
|
|
setTrackingNumber={(value) => {
|
|
setLocalSelectedDewar((prev) => (prev ? { ...prev, tracking_number: value as string } : prev));
|
|
}}
|
|
initialContactPersons={localSelectedDewar?.contact_person ? [localSelectedDewar.contact_person] : []}
|
|
initialReturnAddresses={localSelectedDewar?.return_address ? [localSelectedDewar.return_address] : []}
|
|
defaultContactPerson={localSelectedDewar?.contact_person ?? undefined}
|
|
defaultReturnAddress={localSelectedDewar?.return_address ?? undefined}
|
|
shipmentId={selectedShipment?.id ?? null}
|
|
refreshShipments={refreshShipments}
|
|
/>
|
|
)}
|
|
</React.Fragment>
|
|
))}
|
|
</Stack>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
export default ShipmentDetails; |