aaredb/logistics/src/pages/LogisticsTrackingTab.tsx
2025-02-05 11:55:46 +01:00

558 lines
23 KiB
TypeScript

import React, { useEffect, useState, useRef } from "react";
import { Box, Button, TextField, Typography, Grid, IconButton, Snackbar, Alert } from "@mui/material";
import { CameraAlt } from "@mui/icons-material";
import ScannerModal from "../components/ScannerModal";
import Storage from "../components/Storage";
import { OpenAPI, LogisticsService } from "../../../frontend/openapi";
import type { Slot as SlotSchema, Dewar } from "../../../frontend/openapi/models";
import styled from "styled-components";
import moment from "moment";
import { format } from "date-fns";
// Additional required declarations (map storage settings, props, etc.)
const slotQRCodes = [
"A1-X06SA", "A2-X06SA", "A3-X06SA", "A4-X06SA", "A5-X06SA",
"B1-X06SA", "B2-X06SA", "B3-X06SA", "B4-X06SA", "B5-X06SA",
"C1-X06SA", "C2-X06SA", "C3-X06SA", "C4-X06SA", "C5-X06SA",
"D1-X06SA", "D2-X06SA", "D3-X06SA", "D4-X06SA", "D5-X06SA",
"A1-X10SA", "A2-X10SA", "A3-X10SA", "A4-X10SA", "A5-X10SA",
"B1-X10SA", "B2-X10SA", "B3-X10SA", "B4-X10SA", "B5-X10SA",
"C1-X10SA", "C2-X10SA", "C3-X10SA", "C4-X10SA", "C5-X10SA",
"D1-X10SA", "D2-X10SA", "D3-X10SA", "D4-X10SA", "D5-X10SA",
"NB1", "NB2", "NB3", "NB4", "NB5", "NB6"
];
const beamlineQRCodes = [
"X10SA-Beamline", "X06SA-Beamline", "X06DA-Beamline"
];
const outgoingQRCodes = [
"Outgoing X10SA", "Outgoing X06SA"
];
const storageToSlotsMapping = {
"X06SA-storage": [
"A1-X06SA", "A2-X06SA", "A3-X06SA", "A4-X06SA", "A5-X06SA",
"B1-X06SA", "B2-X06SA", "B3-X06SA", "B4-X06SA", "B5-X06SA",
"C1-X06SA", "C2-X06SA", "C3-X06SA", "C4-X06SA", "C5-X06SA",
"D1-X06SA", "D2-X06SA", "D3-X06SA", "D4-X06SA", "D5-X06SA"
],
"X10SA-storage": [
"A1-X10SA", "A2-X10SA", "A3-X10SA", "A4-X10SA", "A5-X10SA",
"B1-X10SA", "B2-X10SA", "B3-X10SA", "B4-X10SA", "B5-X10SA",
"C1-X10SA", "C2-X10SA", "C3-X10SA", "C4-X10SA", "C5-X10SA",
"D1-X10SA", "D2-X10SA", "D3-X10SA", "D4-X10SA", "D5-X10SA"
],
"Novartis-Box": ["NB1", "NB2", "NB3", "NB4", "NB5", "NB6"]
};
interface SlotData extends SlotSchema {
dewar: Dewar | null;
qr_code: string;
dewar_name?: string;
needsRefillWarning?: boolean;
retrievedTimestamp?: string; // Add timestamp map
beamlineLocation?: string; // Add beamline field
shipment_name?: string; // Add shipment
contact?: string; // Add contact person
local_contact?: string; // Add local contact
Time_until_refill?: number;
}
const DetailPanel = styled(Box)`
width: 100%;
padding: 20px;
background-color: white;
margin-top: 20px;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
`;
const LogisticsTrackingTab: React.FC = () => {
const [dewarQr, setDewarQr] = useState<string | null>(null);
const [locationQr, setLocationQr] = useState<string | null>(null);
const [transactionType, setTransactionType] = useState<string>("");
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
const [selectedSlot, setSelectedSlot] = useState<string | null>(null);
const [sslError, setSslError] = useState<boolean>(false);
const [warningMessage, setWarningMessage] = useState<string | null>(null);
const [selectedSlotData, setSelectedSlotData] = useState<SlotData | null>(null);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
const [slotsData, setSlotsData] = useState<SlotData[]>([]);
useEffect(() => {
// Detect the current environment
const mode = import.meta.env.MODE;
// Dynamically set `OpenAPI.BASE` based on the mode
OpenAPI.BASE =
mode === 'test'
? import.meta.env.VITE_OPENAPI_BASE_TEST
: mode === 'prod'
? import.meta.env.VITE_OPENAPI_BASE_PROD
: import.meta.env.VITE_OPENAPI_BASE_DEV;
// Log warning if `OpenAPI.BASE` is unresolved
if (!OpenAPI.BASE) {
console.error('OpenAPI.BASE is not set. Falling back to a default value.');
OpenAPI.BASE = 'https://default-url.com'; // Use a consistent fallback
}
// Debug for mode and resolved `BASE`
console.log('Environment Mode:', mode);
console.log('Resolved OpenAPI.BASE:', OpenAPI.BASE);
}, []);
const fetchDewarsAndSlots = async () => {
try {
console.log("Fetching dewars and slots...");
setLoading(true);
setError(null);
setWarningMessage(null);
// Fetch dewars and slots in parallel
const [dewars, slots] = await Promise.all([
LogisticsService.getAllDewarsLogisticsDewarsGet(),
LogisticsService.getAllSlotsLogisticsSlotsGet(),
]);
console.log("Dewars fetched:", dewars);
console.log("Slots fetched:", slots);
const dewarMap: { [key: string]: Dewar } = {};
const usedDewarUniqueIds = new Map<string, string>();
// Map dewars by unique_id
dewars.forEach((dewar) => {
if (dewar.unique_id) {
dewarMap[dewar.unique_id] = dewar;
console.log(`Mapped Dewar: ${dewar.unique_id}`);
}
});
// Process and map slots
const newSlotsData = slots.map((slot) => {
let associatedDewar: Dewar | undefined;
// Check if slot has a dewar assigned
if (slot.dewar_unique_id) {
if (usedDewarUniqueIds.has(slot.dewar_unique_id)) {
const existingSlotId = usedDewarUniqueIds.get(slot.dewar_unique_id);
console.warn(`Duplicate dewar assignment: Slot ${slot.id} and Slot ${existingSlotId}`);
setWarningMessage(`Dewar ${slot.dewar_unique_id} is assigned to multiple slots.`);
return { ...slot, occupied: false, dewar: null }; // Mark unoccupied
} else {
associatedDewar = dewarMap[slot.dewar_unique_id];
if (associatedDewar) usedDewarUniqueIds.set(slot.dewar_unique_id, slot.id);
}
}
// Return the enriched slot
return {
...slot,
occupied: !!associatedDewar,
dewar: associatedDewar || null,
dewar_name: associatedDewar?.dewar_name,
needsRefillWarning: !associatedDewar || !slot.time_until_refill,
};
});
// Sort slots by QR code
newSlotsData.sort((a, b) => a.qr_code.localeCompare(b.qr_code));
// Update state
setSlotsData(newSlotsData);
} catch (e) {
console.error("Error fetching dewars/slots:", e);
setError("Failed to load logistics data.");
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchDewarsAndSlots();
}, []);
const formatTimestamp = (timestamp: string | undefined) => {
if (!timestamp) return 'N/A';
const date = new Date(timestamp);
return format(date, 'PPpp', { addSuffix: true });
};
// Reference to the audio element
const audioRef = useRef<HTMLAudioElement | null>(null);
const handleSlotSelection = async (data: { text: string }) => {
const scannedText = data.text.trim();
console.log(`Detected QR code: ${scannedText}`);
// Case 1: Scanned QR code is a Beamline QR Code
if (beamlineQRCodes.includes(scannedText)) {
console.log(`Detected beamline QR code: ${scannedText}`);
// Determine the Dewar ID either from the scanned Dewar or selected slot
const dewarId = dewarQr || slotsData.find(slot => slot.qr_code === selectedSlot)?.dewar?.unique_id;
if (dewarId) {
console.log(`Moving dewar ${dewarId} to beamline ${scannedText}`);
try {
const timestamp = moment().toISOString();
// Assign the dewar to the beamline via POST request
await LogisticsService.scanDewarLogisticsDewarScanPost({
dewar_qr_code: dewarId,
location_qr_code: scannedText,
transaction_type: 'beamline',
timestamp: timestamp,
});
fetchDewarsAndSlots(); // Refresh state
setWarningMessage(`Dewar ${dewarId} successfully moved to beamline ${scannedText}.`);
} catch (e) {
console.error("Error moving Dewar to beamline:", e);
setError("Failed to move Dewar to beamline. Please try again.");
}
} else {
console.error("No Dewar QR code or selected Dewar found.");
alert("Please select or scan a Dewar before scanning the beamline QR code.");
}
return;
}
// Case 2: Scanned QR code is an Outgoing QR Code
if (outgoingQRCodes.includes(scannedText)) {
console.log(`Detected outgoing QR code: ${scannedText}`);
setDewarQr(scannedText);
handleOutgoing();
return;
}
// Case 3: Scanned QR code is a Slot QR Code
if (slotQRCodes.includes(scannedText)) {
console.log(`Detected slot QR code: ${scannedText}`);
const slot = slotsData.find(slot => slot.qr_code === scannedText);
setLocationQr(scannedText);
setSelectedSlot(scannedText);
if (slot?.dewar?.unique_id) {
console.log(`Returning dewar ${slot.dewar.unique_id} to slot ${scannedText}`);
await returnDewarToStorage(slot.dewar.unique_id, scannedText);
} else {
fetchDewarAndAssociate(scannedText);
}
return;
}
// Case 4: Scanned QR code is a Dewar QR Code
console.log("Scanned text is not a slot or beamline. Assuming it is a Dewar QR code.");
try {
const dewar = await LogisticsService.getDewarByUniqueIdLogisticsDewarUniqueIdGet(scannedText);
setDewarQr(dewar.unique_id);
console.log(`Fetched Dewar: ${dewar.unique_id}`);
if (audioRef.current) {
audioRef.current.play();
}
} catch (e) {
console.error("Error fetching Dewar details:", e);
if (e.message.includes("404")) {
alert("Dewar not found for this QR code.");
} else {
setError("Failed to fetch Dewar details. Please try again.");
}
}
};
const returnDewarToStorage = async (dewarId: string, slotQrCode: string) => {
const payload = {
dewar_qr_code: dewarId,
location_qr_code: slotQrCode,
transaction_type: "returned",
};
console.log("Sending payload:", payload);
console.log(`Dewar ID: ${dewarId}, Slot QR Code: ${slotQrCode}`);
try {
await LogisticsService.returnToStorageLogisticsDewarsReturnPost(payload);
fetchDewarsAndSlots();
alert(`Dewar ${dewarId} successfully returned to storage.`);
} catch (error) {
console.error('Failed to return dewar to storage:', error);
if (error.status === 400 && error.response?.data?.detail === "Selected slot is already occupied") {
alert('Selected slot is already occupied. Please choose a different slot.');
} else {
console.error('Unexpected error occurred:', error);
alert('Failed to return dewar to storage.');
}
setError('Failed to return dewar to storage.');
}
};
const handleSlotSelect = (slot: SlotData) => {
if (selectedSlot === slot.qr_code) {
// Deselect if the same slot is clicked again
setSelectedSlot(null);
setLocationQr(null);
setSelectedSlotData(null);
setDewarQr(null); // Clear Dewar QR code
} else {
// Set the selected slot and its data
setSelectedSlot(slot.qr_code);
setLocationQr(slot.qr_code);
setSelectedSlotData(slot);
// If occupied, set the `dewar_unique_id` to the `Dewar QR Code` field
setDewarQr(slot.dewar?.unique_id || null);
}
};
const fetchDewarAndAssociate = async (scannedText: string) => {
try {
const dewar = await LogisticsService.getDewarByUniqueIdLogisticsDewarUniqueIdGet(scannedText);
setDewarQr(dewar.unique_id);
if (audioRef.current) {
audioRef.current.play();
}
} catch (e) {
console.error(e);
if (e.message.includes('SSL')) {
setSslError(true);
} else {
alert('No dewar found with this QR code.');
}
}
};
const handleRefillDewar = async (qrCode?: string) => {
const dewarUniqueId = qrCode || slotsData.find(slot => slot.qr_code === selectedSlot)?.dewar?.unique_id;
if (!dewarUniqueId) {
alert('No dewar associated with the selected slot.');
return;
}
try {
const trimmedUniqueId = dewarUniqueId.trim();
const response = await LogisticsService.refillDewarLogisticsDewarRefillPost(trimmedUniqueId);
if (response && response.time_until_refill) {
alert(`Dewar refilled successfully. Time until next refill: ${response.time_until_refill}`);
} else {
alert('Dewar refilled successfully.');
}
fetchDewarsAndSlots();
} catch (e) {
console.error(e);
alert('Error in refilling dewar');
}
};
const handleSubmit = async () => {
if (!dewarQr || !locationQr || !transactionType) {
alert('All fields are required.');
return;
}
if (!dewarQr.trim()) {
alert('Dewar QR code should not be empty.');
return;
}
const conflictingSlots = slotsData.filter(
slot => slot.dewar?.unique_id === dewarQr && slot.qr_code !== locationQr
);
if (conflictingSlots.length > 0) {
alert(`Dewar ${dewarQr} is already assigned to slot ${conflictingSlots[0].qr_code}. Please resolve the conflict first.`);
return;
}
try {
const timestamp = moment().toISOString();
await LogisticsService.scanDewarLogisticsDewarScanPost({
dewar_qr_code: dewarQr.trim(),
location_qr_code: locationQr.trim(),
transaction_type: transactionType,
timestamp: timestamp,
});
alert('Dewar status updated successfully');
if (audioRef.current) {
audioRef.current.play();
}
fetchDewarsAndSlots();
} catch (e) {
console.error(e);
alert('Error updating dewar status');
}
};
const handleOutgoing = async () => {
if (!dewarQr) {
alert('Scan a dewar QR code first.');
return;
}
try {
await LogisticsService.scanDewarLogisticsDewarScanPost({
dewar_qr_code: dewarQr,
location_qr_code: dewarQr, // Using dewar QR code as location for outgoing
transaction_type: 'outgoing',
timestamp: moment().toISOString(),
});
alert(`Dewar ${dewarQr} is now marked as outgoing.`);
fetchDewarsAndSlots();
} catch (e) {
console.error(e);
alert('Error marking dewar as outgoing');
}
};
return (
<Box>
<Typography variant="h4" gutterBottom>
Logistics Tracking
</Typography>
<Grid container spacing={2}>
<Grid item xs={12} sm={6}>
<Box display="flex" flexDirection="column" alignItems="flex-start">
<Typography variant="h6">Dewar QR Code</Typography>
<TextField
label="Enter Dewar QR Code"
variant="outlined"
fullWidth
value={dewarQr ?? ''}
onChange={(e) => setDewarQr(e.target.value)}
sx={{ mb: 2 }}
/>
<IconButton onClick={() => setIsModalOpen(true)} sx={{ fontSize: 40, mb: 2 }}>
<CameraAlt fontSize="inherit" />
</IconButton>
<Typography variant="h6">Selected Slot</Typography>
<TextField
label="Slot QR Code"
variant="outlined"
fullWidth
value={locationQr ?? ''}
onChange={(e) => setLocationQr(e.target.value)}
sx={{ mb: 2 }}
disabled
/>
<Button
variant="contained"
onClick={() => setTransactionType('incoming')}
color={transactionType === 'incoming' ? 'primary' : 'default'}
sx={{ mb: 1 }}
>
Incoming
</Button>
<Button
variant="contained"
onClick={() => setTransactionType('outgoing')}
color={transactionType === 'outgoing' ? 'primary' : 'default'}
sx={{ mb: 2 }}
>
Outgoing
</Button>
<Button variant="contained" color="primary" onClick={handleSubmit} sx={{ mb: 2 }}>
Submit
</Button>
<Button variant="contained" color="secondary" onClick={() => handleRefillDewar()}>
Refill Dewar
</Button>
</Box>
</Grid>
<Grid item xs={12} sm={6}>
{selectedSlotData ? (
<Grid item xs={12} sm={6}>
{selectedSlotData ? (
<DetailPanel>
<Typography variant="h6">{selectedSlotData.label}</Typography>
<Typography variant="body2">{`Shipment: ${selectedSlotData.shipment_name}`}</Typography>
<Typography variant="body2">{`Dewar: ${selectedSlotData.dewar_name || 'N/A'}`}</Typography>
<Typography variant="body2">{`Contact Person: ${selectedSlotData.contact}`}</Typography>
<Typography variant="body2">{`QR Code: ${selectedSlotData.qr_code}`}</Typography>
<Typography variant="body2">{`Occupied: ${selectedSlotData.occupied ? 'Yes' : 'No'}`}</Typography>
<Typography variant="body2">{`Needs Refill: ${selectedSlotData.needsRefillWarning ? 'Yes' : 'No'}`}</Typography>
<Typography variant="body2">{`Time Until Refill: ${selectedSlotData.time_until_refill ?? 'N/A'}`}</Typography>
<Typography variant="body2">{`Last Event: ${formatTimestamp(selectedSlotData.retrievedTimestamp)}`}</Typography>
<Typography variant="body2">{`Local Contact: ${selectedSlotData.local_contact}`}</Typography>
<Typography variant="body2">{`Beamline Location: ${selectedSlotData.beamlineLocation || 'N/A'}`}</Typography>
</DetailPanel>
) : (
<Typography variant="h6">Select a slot to see more details.</Typography>
)}
</Grid>
) : (
<Typography>No slot selected</Typography>
)}
</Grid>
</Grid>
{loading ? (
<Typography mt={2}>Loading...</Typography>
) : error ? (
<Typography mt={2} color="error">{error}</Typography>
) : (
<Box>
{['X06SA-storage', 'X10SA-storage', 'Novartis-Box'].map((storageKey) => {
const filteredSlots = slotsData.filter((slot) => storageToSlotsMapping[storageKey].includes(slot.qr_code));
return (
<Storage
key={storageKey}
name={storageKey}
selectedSlot={selectedSlot}
slotsData={filteredSlots}
onSelectSlot={handleSlotSelect}
onRefillDewar={handleRefillDewar}
/>
);
})}
</Box>
)}
<ScannerModal open={isModalOpen} onClose={() => setIsModalOpen(false)} onScan={handleSlotSelection} />
<Snackbar open={sslError} autoHideDuration={6000} onClose={() => setSslError(false)}>
<Alert onClose={() => setSslError(false)} severity="error">
SSL Error: Unable to establish a secure connection with the server.
</Alert>
</Snackbar>
{warningMessage && (
<Snackbar open autoHideDuration={6000} onClose={() => setWarningMessage(null)}>
<Alert severity="warning" onClose={() => setWarningMessage(null)}>
{warningMessage}
</Alert>
</Snackbar>
)}
{error && (
<Snackbar open autoHideDuration={6000} onClose={() => setError(null)}>
<Alert severity="error" onClose={() => setError(null)}>
{error}
</Alert>
</Snackbar>
)}
<audio ref={audioRef} src="src/assets/50565__broumbroum__sf3-sfx-menu-validate.wav" preload="auto" />
</Box>
);
};
export default LogisticsTrackingTab;