added dewar type, serial number, generate unique id, qr code and generate label

This commit is contained in:
GotthardG
2024-11-14 23:17:20 +01:00
parent ca11a359f9
commit 6083c72a1d
8 changed files with 684 additions and 181 deletions

View File

@ -1,9 +1,34 @@
import React, { useState, useEffect } from 'react';
import { Box, Typography, TextField, Button, Select, MenuItem, Snackbar } from '@mui/material';
import React, { useRef, useState, useEffect } from 'react';
import {
Box,
Typography,
TextField,
Button,
Select,
MenuItem,
Snackbar,
FormControl,
InputLabel,
IconButton,
Tooltip,
Alert,
} from '@mui/material';
import QRCode from 'react-qr-code';
import { ContactPerson, Address, Dewar, ContactsService, AddressesService, DewarsService, ShipmentsService } from '../../openapi'; // Adjust path if necessary
import Unipuck from '../components/Unipuck'; // This path should be checked and corrected if necessary
import { Shipment } from "../types.ts"; // Correct or adjust as needed
import {
Dewar,
DewarType,
DewarSerialNumber,
ContactPerson,
Address,
ContactsService,
AddressesService,
DewarsService,
ShipmentsService,
} from '../../openapi';
import Unipuck from '../components/Unipuck';
import { saveAs } from 'file-saver';
import DownloadIcon from '@mui/icons-material/Download';
interface DewarDetailsProps {
dewar: Dewar;
@ -14,7 +39,6 @@ interface DewarDetailsProps {
defaultContactPerson?: ContactPerson;
defaultReturnAddress?: Address;
shipmentId: number;
selectedShipment?: Shipment;
}
interface NewContactPerson {
@ -51,32 +75,86 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
const [isCreatingContactPerson, setIsCreatingContactPerson] = useState(false);
const [isCreatingReturnAddress, setIsCreatingReturnAddress] = useState(false);
const [puckStatuses, setPuckStatuses] = useState<string[][]>([]);
const [newContactPerson, setNewContactPerson] = useState<NewContactPerson>({ id: 0, firstName: '', lastName: '', phone_number: '', email: '' });
const [newReturnAddress, setNewReturnAddress] = useState<NewReturnAddress>({ id: 0, street: '', city: '', zipcode: '', country: '' });
const [newContactPerson, setNewContactPerson] = useState<NewContactPerson>({
id: 0,
firstName: '',
lastName: '',
phone_number: '',
email: '',
});
const [newReturnAddress, setNewReturnAddress] = useState<NewReturnAddress>({
id: 0,
street: '',
city: '',
zipcode: '',
country: '',
});
const [changesMade, setChangesMade] = useState(false);
const [feedbackMessage, setFeedbackMessage] = useState('');
const [openSnackbar, setOpenSnackbar] = useState(false);
const [feedbackMessage, setFeedbackMessage] = useState<string>('');
const [openSnackbar, setOpenSnackbar] = useState<boolean>(false);
const [newDewarType, setNewDewarType] = useState<string>('');
const [newDewarSerialNumber, setNewDewarSerialNumber] = useState<string>('');
const [selectedDewarType, setSelectedDewarType] = useState<string>(dewar.dewar_type_id?.toString() || '');
const [knownDewarTypes, setKnownDewarTypes] = useState<DewarType[]>([]);
const [knownSerialNumbers, setKnownSerialNumbers] = useState<DewarSerialNumber[]>([]);
const [selectedSerialNumber, setSelectedSerialNumber] = useState<string>('');
const [isQRCodeGenerated, setIsQRCodeGenerated] = useState(false);
const [qrCodeValue, setQrCodeValue] = useState(dewar.qrcode || '');
const qrCodeRef = useRef<HTMLCanvasElement>(null); //
useEffect(() => {
const fetchDewarTypes = async () => {
try {
const response = await DewarsService.getDewarTypesDewarsDewarTypesGet();
setKnownDewarTypes(response ?? []);
} catch (error) {
setFeedbackMessage('Failed to fetch dewar types.');
setOpenSnackbar(true);
console.error('Error fetching dewar types:', error);
}
};
fetchDewarTypes();
}, []);
// Fetch known serial numbers
useEffect(() => {
const fetchSerialNumbers = async () => {
try {
const response = await DewarsService.getAllSerialNumbersDewarsDewarSerialNumbersGet();
setKnownSerialNumbers(response ?? []);
} catch (error) {
setFeedbackMessage('Failed to fetch serial numbers.');
setOpenSnackbar(true);
console.error('Error fetching serial numbers:', error);
}
};
fetchSerialNumbers();
}, []);
useEffect(() => {
setLocalTrackingNumber(dewar.tracking_number || '');
const setInitialContactPerson = () => {
setSelectedContactPerson(
dewar.contact_person?.id?.toString() ||
defaultContactPerson?.id?.toString() ||
''
dewar.contact_person?.id?.toString() || defaultContactPerson?.id?.toString() || ''
);
};
const setInitialReturnAddress = () => {
setSelectedReturnAddress(
dewar.return_address?.id?.toString() ||
defaultReturnAddress?.id?.toString() ||
''
dewar.return_address?.id?.toString() || defaultReturnAddress?.id?.toString() || ''
);
};
setLocalTrackingNumber(dewar.tracking_number || '');
setInitialContactPerson();
setInitialReturnAddress();
if (dewar.dewar_type_id) {
setSelectedDewarType(dewar.dewar_type_id.toString());
}
if (dewar.dewar_serial_number_id) {
setSelectedSerialNumber(dewar.dewar_serial_number_id.toString());
}
}, [dewar, defaultContactPerson, defaultReturnAddress]);
useEffect(() => {
@ -108,17 +186,19 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
const fetchSamples = async () => {
if (dewar.id) {
try {
const fetchedSamples = await ShipmentsService.getSamplesInDewarShipmentsShipmentsShipmentIdDewarsDewarIdSamplesGet(shipmentId, dewar.id);
console.log("Fetched Samples: ", fetchedSamples);
const fetchedSamples = await ShipmentsService.getSamplesInDewarShipmentsShipmentsShipmentIdDewarsDewarIdSamplesGet(
shipmentId,
dewar.id
);
const updatedPuckStatuses = (dewar.pucks ?? []).map(puck => {
const puckSamples = fetchedSamples.filter(sample => sample.puck_id === puck.id);
const updatedPuckStatuses = (dewar.pucks ?? []).map((puck) => {
const puckSamples = fetchedSamples.filter((sample) => sample.puck_id === puck.id);
const statusArray = Array(16).fill('empty');
puckSamples.forEach(sample => {
puckSamples.forEach((sample) => {
if (sample.position >= 1 && sample.position <= 16) {
statusArray[sample.position - 1] = 'filled'; // Corrected line
statusArray[sample.position - 1] = 'filled';
}
});
@ -127,7 +207,7 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
setPuckStatuses(updatedPuckStatuses);
} catch (error) {
console.error("Error fetching samples:", error);
console.error('Error fetching samples:', error);
}
}
};
@ -135,15 +215,62 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
fetchSamples();
}, [dewar, shipmentId]);
useEffect(() => {
setSelectedDewarType(
knownDewarTypes.find((type) => type.id === dewar.dewar_type_id)?.id.toString() || ''
);
}, [knownDewarTypes, dewar.dewar_type_id]);
useEffect(() => {
setSelectedSerialNumber(
knownSerialNumbers.find((sn) => sn.id === dewar.dewar_serial_number_id)?.id.toString() || ''
);
}, [knownSerialNumbers, dewar.dewar_serial_number_id]);
const validateEmail = (email: string) => /\S+@\S+\.\S+/.test(email);
const validatePhoneNumber = (phone: string) => /^\+?[1-9]\d{1,14}$/.test(phone);
const validateZipCode = (zipcode: string) => /^\d{5}(?:[-\s]\d{4})?$/.test(zipcode);
if (!dewar) return <Typography>No dewar selected.</Typography>;
const handleSaveNewDewarTypeAndSerialNumber = async () => {
if (newDewarType) {
try {
const typeResponse = await DewarsService.createDewarTypeDewarsDewarTypesPost({ dewar_type: newDewarType });
const serialResponse = await DewarsService.createDewarSerialNumberDewarsDewarSerialNumbersPost({
serial_number: newDewarSerialNumber,
dewar_type_id: typeResponse.id,
});
setKnownDewarTypes([...knownDewarTypes, typeResponse]);
setKnownSerialNumbers([...knownSerialNumbers, serialResponse]);
setSelectedDewarType(typeResponse.id.toString());
setSelectedSerialNumber(serialResponse.serial_number);
setNewDewarType('');
setNewDewarSerialNumber('');
setChangesMade(true);
} catch (error) {
setFeedbackMessage('Failed to save new dewar type and serial number.');
setOpenSnackbar(true);
}
} else if (newDewarSerialNumber && selectedDewarType) {
try {
const response = await DewarsService.createDewarSerialNumberDewarsDewarSerialNumbersPost({
serial_number: newDewarSerialNumber,
dewar_type_id: parseInt(selectedDewarType, 10),
});
setKnownSerialNumbers([...knownSerialNumbers, response]);
setSelectedSerialNumber(response.serial_number);
setNewDewarSerialNumber('');
setChangesMade(true);
} catch (error) {
setFeedbackMessage('Failed to save new serial number.');
setOpenSnackbar(true);
}
}
};
const handleAddContact = async () => {
if (!validateEmail(newContactPerson.email) || !validatePhoneNumber(newContactPerson.phone_number) ||
!newContactPerson.firstName || !newContactPerson.lastName) {
if (!validateEmail(newContactPerson.email) || !validatePhoneNumber(newContactPerson.phone_number) || !newContactPerson.firstName || !newContactPerson.lastName) {
setFeedbackMessage('Please fill in all new contact person fields correctly.');
setOpenSnackbar(true);
return;
@ -189,7 +316,13 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
const a = await AddressesService.createReturnAddressAddressesPost(payload);
setReturnAddresses([...returnAddresses, a]);
setFeedbackMessage('Return address added successfully.');
setNewReturnAddress({ id: 0, street: '', city: '', zipcode: '', country: '' });
setNewReturnAddress({
id: 0,
street: '',
city: '',
zipcode: '',
country: '',
});
setSelectedReturnAddress(a.id?.toString() || '');
} catch {
setFeedbackMessage('Failed to create a new return address. Please try again later.');
@ -209,7 +342,7 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
};
if (!selectedContactPerson || !selectedReturnAddress) {
setFeedbackMessage("Please ensure all required fields are filled.");
setFeedbackMessage('Please ensure all required fields are filled.');
setOpenSnackbar(true);
return;
}
@ -217,15 +350,16 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
const dewarId = dewar.id;
if (!dewarId) {
setFeedbackMessage("Invalid Dewar ID. Please ensure Dewar ID is provided.");
setFeedbackMessage('Invalid Dewar ID. Please ensure Dewar ID is provided.');
setOpenSnackbar(true);
return;
}
try {
const payload = {
dewar_id: dewarId,
dewar_name: dewar.dewar_name,
dewar_type_id: parseInt(selectedDewarType, 10),
dewar_serial_number_id: parseInt(selectedSerialNumber, 10),
tracking_number: localTrackingNumber,
number_of_pucks: dewar.number_of_pucks,
number_of_samples: dewar.number_of_samples,
@ -240,10 +374,60 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
};
await DewarsService.updateDewarDewarsDewarIdPut(dewarId, payload);
setFeedbackMessage("Changes saved successfully.");
setFeedbackMessage('Changes saved successfully.');
setChangesMade(false);
} catch (error) {
setFeedbackMessage("Failed to save changes. Please try again later.");
setFeedbackMessage('Failed to save changes. Please try again later.');
setOpenSnackbar(true);
}
};
const handleSerialNumberChange = (value: string) => {
setSelectedSerialNumber(value);
const serialNumber = knownSerialNumbers.find((sn) => sn.id.toString() === value);
if (serialNumber) {
setSelectedDewarType(serialNumber.dewar_type_id.toString());
}
setChangesMade(true);
};
const handleGenerateQRCode = async () => {
if (!dewar) return;
try {
const response = await DewarsService.generateDewarQrcodeDewarsDewarIdGenerateQrcodePost(dewar.id);
setQrCodeValue(response.qrcode); // assuming the backend returns the QR code value
setIsQRCodeGenerated(true); // to track the state if the QR code is generated
setFeedbackMessage("QR Code generated successfully");
setOpenSnackbar(true);
} catch (error) {
console.error("Failed to generate QR code:", error);
setFeedbackMessage("QR Code generation failed");
setOpenSnackbar(true);
}
};
const handleDownloadLabel = async () => {
if (!dewar) return;
try {
const response = await DewarsService.downloadDewarLabelDewarsDewarIdDownloadLabelGet(dewar.id);
// The response object might need parsing
const blob = new Blob([response as any], { type: 'application/pdf' });
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', `dewar_label_${dewar.id}.pdf`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
setFeedbackMessage("Label downloaded successfully");
setOpenSnackbar(true);
} catch (error) {
console.error("Failed to download label:", error);
setFeedbackMessage("Label download failed");
setOpenSnackbar(true);
}
};
@ -254,7 +438,7 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
<TextField
label="Tracking Number"
value={localTrackingNumber}
onChange={e => {
onChange={(e) => {
setLocalTrackingNumber(e.target.value);
setTrackingNumber(e.target.value);
setChangesMade(true);
@ -262,41 +446,121 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
variant="outlined"
sx={{ width: '300px', marginBottom: 2 }}
/>
<Box sx={{ display: 'flex', alignItems: 'center', marginBottom: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', flexDirection: 'column' }}>
{dewar.qrcode ? (
<QRCode value={dewar.qrcode} size={70} />
<FormControl variant="outlined" sx={{ width: '300px', marginBottom: 2 }}>
<InputLabel id="dewar-serial-number-label">Dewar Serial Number</InputLabel>
<Select
labelId="dewar-serial-number-label"
label="Dewar Serial Number"
value={selectedSerialNumber}
onChange={(e) => handleSerialNumberChange(e.target.value as string)}
displayEmpty
>
{knownSerialNumbers.map((sn) => (
<MenuItem key={sn.id} value={sn.id.toString()}>
{sn.serial_number}
</MenuItem>
))}
<MenuItem value="add">Add New Serial Number</MenuItem>
</Select>
{selectedSerialNumber === 'add' && (
<Box>
<FormControl variant="outlined" sx={{ width: '300px', marginBottom: 2 }}>
<InputLabel id="dewar-type-label">Dewar Type</InputLabel>
<Select
labelId="dewar-type-label"
label="Dewar Type"
value={selectedDewarType}
onChange={(e) => {
const value = e.target.value as string;
setSelectedDewarType(value);
setChangesMade(true);
}}
displayEmpty
>
{knownDewarTypes.map((type) => (
<MenuItem key={type.id} value={type.id.toString()}>
{type.dewar_type}
</MenuItem>
))}
<MenuItem value="add">Add New Dewar Type</MenuItem>
</Select>
{selectedDewarType === 'add' && (
<Box>
<TextField
label="Add New Dewar Type"
value={newDewarType}
onChange={(e) => setNewDewarType(e.target.value)}
variant="outlined"
sx={{ width: '300px', marginBottom: 2 }}
/>
</Box>
)}
</FormControl>
<TextField
label="Add New Serial Number"
value={newDewarSerialNumber}
onChange={(e) => setNewDewarSerialNumber(e.target.value)}
variant="outlined"
sx={{ width: '300px', marginBottom: 2 }}
/>
<Button onClick={handleSaveNewDewarTypeAndSerialNumber} variant="contained">
Save Dewar Type and Serial Number
</Button>
</Box>
)}
</FormControl>
<Box sx={{ marginTop: 2 }}>
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', marginBottom: 2 }}>
{qrCodeValue ? (
<Box sx={{ textAlign: 'center', marginBottom: 2 }}>
<QRCode id="qrCodeCanvas" value={qrCodeValue} size={150} />
<Box sx={{ display: 'flex', alignItems: 'center', marginTop: 1 }}>
<Tooltip title="Download Label">
<IconButton onClick={handleDownloadLabel} sx={{ transform: 'scale(1.5)', margin: 1 }}>
<DownloadIcon />
</IconButton>
</Tooltip>
<Typography variant="body2">Label is ready for download</Typography>
</Box>
</Box>
) : (
<Typography>No QR code available</Typography>
)}
<Button variant="contained" sx={{ marginTop: 1 }} onClick={() => { /* Add logic to generate QR Code */ }}>
<Button
variant="contained"
sx={{ marginTop: 1 }}
onClick={handleGenerateQRCode}
>
Generate QR Code
</Button>
</Box>
</Box>
<Box sx={{ marginTop: 2 }}>
<Typography variant="body1">Number of Pucks: {dewar.number_of_pucks}</Typography>
{(dewar.pucks ?? []).length > 0
? <Unipuck pucks={(dewar.pucks ?? []).length} samples={puckStatuses} />
: <Typography>No pucks attached to the dewar.</Typography>}
{(dewar.pucks ?? []).length > 0 ? (
<Unipuck pucks={(dewar.pucks ?? []).length} samples={puckStatuses} />
) : (
<Typography>No pucks attached to the dewar.</Typography>
)}
<Typography variant="body1">Number of Samples: {dewar.number_of_samples}</Typography>
</Box>
<Typography variant="body1">Current Contact Person:</Typography>
<Select
value={selectedContactPerson}
onChange={e => {
onChange={(e) => {
const value = e.target.value;
setSelectedContactPerson(value);
setIsCreatingContactPerson(value === 'add');
setChangesMade(true);
}}
fullWidth
sx={{ marginBottom: 2 }}
variant="outlined"
displayEmpty
sx={{ width: '300px', marginBottom: 2 }}
>
{contactPersons.map(person => (
<MenuItem key={person.id?.toString()} value={person.id?.toString() || ''}>
{contactPersons.map((person) => (
<MenuItem key={person.id} value={person.id?.toString()}>
{person.firstname} {person.lastname}
</MenuItem>
))}
@ -307,61 +571,72 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
<TextField
label="First Name"
value={newContactPerson.firstName}
onChange={e => setNewContactPerson({ ...newContactPerson, firstName: e.target.value })}
onChange={(e) =>
setNewContactPerson((prev) => ({
...prev,
firstName: e.target.value,
}))
}
variant="outlined"
fullWidth
sx={{ marginBottom: 1 }}
sx={{ width: '300px', marginBottom: 2 }}
/>
<TextField
label="Last Name"
value={newContactPerson.lastName}
onChange={e => setNewContactPerson({ ...newContactPerson, lastName: e.target.value })}
onChange={(e) =>
setNewContactPerson((prev) => ({
...prev,
lastName: e.target.value,
}))
}
variant="outlined"
fullWidth
sx={{ marginBottom: 1 }}
sx={{ width: '300px', marginBottom: 2 }}
/>
<TextField
label="Phone"
label="Phone Number"
value={newContactPerson.phone_number}
onChange={e => setNewContactPerson({ ...newContactPerson, phone_number: e.target.value })}
onChange={(e) =>
setNewContactPerson((prev) => ({
...prev,
phone_number: e.target.value,
}))
}
variant="outlined"
fullWidth
sx={{ marginBottom: 1 }}
error={!validatePhoneNumber(newContactPerson.phone_number)}
helperText={!validatePhoneNumber(newContactPerson.phone_number) ? "Invalid phone number" : ""}
sx={{ width: '300px', marginBottom: 2 }}
/>
<TextField
label="Email"
value={newContactPerson.email}
onChange={e => setNewContactPerson({ ...newContactPerson, email: e.target.value })}
onChange={(e) =>
setNewContactPerson((prev) => ({
...prev,
email: e.target.value,
}))
}
variant="outlined"
fullWidth
sx={{ marginBottom: 1 }}
error={!validateEmail(newContactPerson.email)}
helperText={!validateEmail(newContactPerson.email) ? "Invalid email" : ""}
sx={{ width: '300px', marginBottom: 2 }}
/>
<Button variant="contained" onClick={handleAddContact}>
Save Contact Person
<Button onClick={handleAddContact} variant="contained">
Save New Contact Person
</Button>
</Box>
)}
<Typography variant="body1">Current Return Address:</Typography>
<Select
value={selectedReturnAddress}
onChange={e => {
onChange={(e) => {
const value = e.target.value;
setSelectedReturnAddress(value);
setIsCreatingReturnAddress(value === 'add');
setChangesMade(true);
}}
fullWidth
sx={{ marginBottom: 2 }}
variant="outlined"
displayEmpty
sx={{ width: '300px', marginBottom: 2 }}
>
{returnAddresses.map(address => (
<MenuItem key={address.id?.toString()} value={address.id?.toString() || ''}>
{address.street}, {address.city}
{returnAddresses.map((address) => (
<MenuItem key={address.id} value={address.id?.toString()}>
{address.street}, {address.city}, {address.zipcode}, {address.country}
</MenuItem>
))}
<MenuItem value="add">Add New Return Address</MenuItem>
@ -371,53 +646,77 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
<TextField
label="Street"
value={newReturnAddress.street}
onChange={e => setNewReturnAddress({ ...newReturnAddress, street: e.target.value })}
onChange={(e) =>
setNewReturnAddress((prev) => ({
...prev,
street: e.target.value,
}))
}
variant="outlined"
fullWidth
sx={{ marginBottom: 1 }}
sx={{ width: '300px', marginBottom: 2 }}
/>
<TextField
label="City"
value={newReturnAddress.city}
onChange={e => setNewReturnAddress({ ...newReturnAddress, city: e.target.value })}
onChange={(e) =>
setNewReturnAddress((prev) => ({
...prev,
city: e.target.value,
}))
}
variant="outlined"
fullWidth
sx={{ marginBottom: 1 }}
sx={{ width: '300px', marginBottom: 2 }}
/>
<TextField
label="Zip Code"
value={newReturnAddress.zipcode}
onChange={e => setNewReturnAddress({ ...newReturnAddress, zipcode: e.target.value })}
onChange={(e) =>
setNewReturnAddress((prev) => ({
...prev,
zipcode: e.target.value,
}))
}
variant="outlined"
fullWidth
sx={{ marginBottom: 1 }}
error={!validateZipCode(newReturnAddress.zipcode)}
helperText={!validateZipCode(newReturnAddress.zipcode) ? "Invalid zip code" : ""}
sx={{ width: '300px', marginBottom: 2 }}
/>
<TextField
label="Country"
value={newReturnAddress.country}
onChange={e => setNewReturnAddress({ ...newReturnAddress, country: e.target.value })}
onChange={(e) =>
setNewReturnAddress((prev) => ({
...prev,
country: e.target.value,
}))
}
variant="outlined"
fullWidth
sx={{ marginBottom: 1 }}
sx={{ width: '300px', marginBottom: 2 }}
/>
<Button variant="contained" onClick={handleAddAddress}>
Save Return Address
<Button onClick={handleAddAddress} variant="contained">
Save New Return Address
</Button>
</Box>
)}
{changesMade && (
<Button variant="contained" color="primary" onClick={handleSaveChanges} sx={{ marginTop: 2 }}>
<Box sx={{ marginTop: 2 }}>
<Button
variant="contained"
color="primary"
onClick={handleSaveChanges}
disabled={!changesMade}
>
Save Changes
</Button>
)}
</Box>
<Snackbar
open={openSnackbar}
autoHideDuration={6000}
onClose={() => setOpenSnackbar(false)}
message={feedbackMessage}
/>
>
<Alert onClose={() => setOpenSnackbar(false)} severity="info" sx={{ width: '100%' }}>
{feedbackMessage}
</Alert>
</Snackbar>
</Box>
);
};