
Updated Dewar API methods to use protected endpoints for enhanced security and consistency. Added `pgroups` handling in various frontend components and modified the LogisticsView contact field for clarity. Simplified backend router imports for better readability.
445 lines
21 KiB
TypeScript
445 lines
21 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import {Box, Typography, Button, Stack, TextField, IconButton, Grid, Chip} 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, Contact, ApiError, ShipmentsService } from "../../openapi";
|
|
import { SxProps } from "@mui/system";
|
|
import CustomStepper from "./DewarStepper";
|
|
import DewarDetails from './DewarDetails';
|
|
import { PuckDetailsVisual } from '../assets/icons/SimplePuckIcon';
|
|
import CrystalFacetedIcon from "../assets/icons/CrystalIcon.tsx";
|
|
|
|
|
|
const MAX_COMMENTS_LENGTH = 200;
|
|
|
|
interface ShipmentDetailsProps {
|
|
activePgroup: string;
|
|
pgroups: string;
|
|
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;
|
|
defaultContact?: Contact;
|
|
}
|
|
|
|
const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
|
|
pgroups,
|
|
activePgroup,
|
|
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 || '');
|
|
|
|
console.log('Active Pgroup:', activePgroup); // Debugging or use it where required
|
|
|
|
const initialNewDewarState: Partial<Dewar> = {
|
|
dewar_name: '',
|
|
tracking_number: '',
|
|
number_of_pucks: 0,
|
|
number_of_samples: 0,
|
|
contact_id: selectedShipment?.contact?.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_id: selectedShipment?.contact?.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.removeDewarFromShipmentProtectedShipmentsShipmentIdRemoveDewarDewarIdDelete(
|
|
selectedShipment.id,
|
|
dewarId
|
|
);
|
|
setSelectedShipment(updatedShipment);
|
|
setLocalSelectedDewar(null);
|
|
refreshShipments();
|
|
alert('Dewar deleted successfully!');
|
|
} catch (error: any) {
|
|
console.error('Full error object:', error);
|
|
|
|
let errorMessage = 'Failed to delete dewar. Please try again.';
|
|
|
|
if (error instanceof ApiError && error.body) {
|
|
console.error('API error body:', error.body);
|
|
errorMessage = error.body.detail || errorMessage;
|
|
} else if (error.message) {
|
|
errorMessage = error.message;
|
|
}
|
|
|
|
alert(`Failed to delete dewar: ${errorMessage}`);
|
|
}
|
|
}
|
|
};
|
|
|
|
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,
|
|
pgroups:activePgroup,
|
|
dewar_name: newDewar.dewar_name.trim(),
|
|
contact_id: selectedShipment?.contact?.id,
|
|
return_address_id: selectedShipment?.return_address?.id,
|
|
status: 'active',
|
|
} as Dewar;
|
|
|
|
if (!newDewarToPost.dewar_name || !newDewarToPost.status) {
|
|
throw new Error('Missing required fields');
|
|
}
|
|
|
|
const createdDewar = await DewarsService.createOrUpdateDewarProtectedDewarsPost(selectedShipment.id, newDewarToPost);
|
|
|
|
if (createdDewar && selectedShipment) {
|
|
const updatedShipment = await ShipmentsService.addDewarToShipmentProtectedShipmentsShipmentIdAddDewarPost(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.updateShipmentCommentsProtectedShipmentsShipmentIdCommentsPut(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 contact = selectedShipment?.contact;
|
|
|
|
const renderPgroupChips = () => {
|
|
// Safely handle pgroups as an array
|
|
const pgroupsArray = Array.isArray(selectedShipment?.pgroups)
|
|
? selectedShipment.pgroups
|
|
: selectedShipment?.pgroups?.split(",").map((pgroup: string) => pgroup.trim()) || [];
|
|
|
|
if (!pgroupsArray.length) {
|
|
return <Typography variant="body2">No associated pgroups</Typography>;
|
|
}
|
|
|
|
return pgroupsArray.map((pgroup: string) => (
|
|
<Chip
|
|
key={pgroup}
|
|
label={pgroup}
|
|
color={pgroup === activePgroup ? "primary" : "default"} // Highlight active pgroups
|
|
sx={{
|
|
margin: 0.5,
|
|
backgroundColor: pgroup === activePgroup ? '#19d238' : '#b0b0b0',
|
|
color: pgroup === activePgroup ? 'white' : 'black',
|
|
fontWeight: 'bold',
|
|
borderRadius: '8px',
|
|
height: '20px',
|
|
fontSize: '12px',
|
|
boxShadow: '0px 1px 3px rgba(0, 0, 0, 0.2)',
|
|
//cursor: isAssociated ? 'default' : 'pointer', // Disable pointer for associated chips
|
|
//'&:hover': { opacity: isAssociated ? 1 : 0.8 }, // Disable hover effect for associated chips
|
|
mr: 1,
|
|
mb: 1,
|
|
}}
|
|
/>
|
|
));
|
|
};
|
|
|
|
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>
|
|
<Box sx={{ display: 'flex', flexWrap: 'wrap' }}>
|
|
{renderPgroupChips()}
|
|
</Box>
|
|
<Typography variant="body1" color="textSecondary">
|
|
Main contact person: {contact ? `${contact.firstname} ${contact.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', // Flex container to align all items horizontally
|
|
alignItems: 'center', // Vertically align items in the center
|
|
justifyContent: 'space-between', // Distribute children evenly across the row
|
|
width: '100%', // Ensure the container spans full width
|
|
gap: 2, // Add consistent spacing between sections
|
|
}}
|
|
>
|
|
{/* Left: QR Code */}
|
|
<Box>
|
|
{dewar.unique_id ? (
|
|
<QRCode value={dewar.unique_id} size={60} />
|
|
) : (
|
|
<Box
|
|
sx={{
|
|
width: 60,
|
|
height: 60,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
border: '1px dashed #ccc',
|
|
borderRadius: 1,
|
|
color: 'text.secondary',
|
|
}}
|
|
>
|
|
<Typography variant="body2">No QR Code</Typography>
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
|
|
{/* Middle-Left: Dewar Information */}
|
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
|
<Typography variant="h6" fontWeight="bold">
|
|
{dewar.dewar_name}
|
|
</Typography>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
<PuckDetailsVisual puckCount={dewar.number_of_pucks || 0} />
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
|
<CrystalFacetedIcon size={20} />
|
|
<Typography variant="body2">{dewar.number_of_samples || 0} Samples</Typography>
|
|
</Box>
|
|
</Box>
|
|
</Box>
|
|
|
|
{/* Middle-Right: Contact and Return Information */}
|
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, alignItems: 'flex-start' }}>
|
|
<Typography variant="body2" color="text.secondary">
|
|
Contact Person: {dewar.contact?.firstname ? `${dewar.contact.firstname} ${dewar.contact.lastname}` : 'N/A'}
|
|
</Typography>
|
|
<Typography variant="body2" color="text.secondary">
|
|
Return Address: {dewar.return_address?.house_number
|
|
? `${dewar.return_address.street}, ${dewar.return_address.city}`
|
|
: 'N/A'}
|
|
</Typography>
|
|
</Box>
|
|
|
|
{/* Right: Stepper */}
|
|
<Box
|
|
sx={{
|
|
flexGrow: 1, // Allow the stepper to expand and use space effectively
|
|
maxWidth: '400px', // Optional: Limit how wide the stepper can grow
|
|
}}
|
|
>
|
|
<CustomStepper
|
|
dewar={dewar}
|
|
selectedDewarId={localSelectedDewar?.id ?? null}
|
|
refreshShipments={refreshShipments}
|
|
sx={{ width: '100%' }} // Make the stepper fill its container
|
|
/>
|
|
</Box>
|
|
</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
|
|
pgroups={pgroups}
|
|
activePgroup={activePgroup}
|
|
dewar={localSelectedDewar}
|
|
trackingNumber={localSelectedDewar?.tracking_number || ''}
|
|
setTrackingNumber={(value) => {
|
|
setLocalSelectedDewar((prev) => (prev ? { ...prev, tracking_number: value as string } : prev));
|
|
}}
|
|
initialContacts={localSelectedDewar?.contact ? [localSelectedDewar.contact] : []}
|
|
initialReturnAddresses={localSelectedDewar?.return_address ? [localSelectedDewar.return_address] : []}
|
|
defaultContact={localSelectedDewar?.contact ?? undefined}
|
|
defaultReturnAddress={localSelectedDewar?.return_address ?? undefined}
|
|
shipmentId={selectedShipment?.id ?? null}
|
|
refreshShipments={refreshShipments}
|
|
/>
|
|
)}
|
|
</React.Fragment>
|
|
))}
|
|
</Stack>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
export default ShipmentDetails; |