aaredb/frontend/src/components/ShipmentDetails.tsx
GotthardG c2215860bf Refactor Dewar service methods and improve field handling
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.
2025-01-30 13:39:49 +01:00

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;