diff --git a/backend/app/models.py b/backend/app/models.py index dbeef91..c793435 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -214,3 +214,26 @@ class PuckEvent(Base): timestamp = Column(DateTime, default=datetime.now) puck = relationship("Puck", back_populates="events") + + +# class Results(Base): +# __tablename__ = "results" +# +# id = Column(Integer, primary_key=True, index=True, autoincrement=True) +# pgroup = Column(String(255), nullable=False) +# sample_id = Column(Integer, ForeignKey("samples.id"), nullable=False) +# method = Column(String(255), nullable=False) +# #resolution: Column(Float(255), nullable=False) +# unit_cell: str +# spacegroup: str +# rmerge: float +# rmeas: float +# isig: float +# cc: float +# cchalf: float +# completeness: float +# multiplicity: float +# nobs: int +# total_refl: int +# unique_refl: int +# #comments: Optional[constr(max_length=200)] = None diff --git a/backend/app/schemas.py b/backend/app/schemas.py index efefe32..ca68430 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -1,4 +1,4 @@ -from typing import List, Optional, ClassVar +from typing import List, Optional from datetime import datetime from pydantic import BaseModel, EmailStr, constr, Field, field_validator from datetime import date @@ -362,8 +362,28 @@ class SampleEventResponse(BaseModel): class Results(BaseModel): + id: int + pgroup: str + sample_id: int + method: str + resolution: float + unit_cell: str + spacegroup: str + rmerge: float + rmeas: float + isig: float + cc: float + cchalf: float + completeness: float + multiplicity: float + nobs: int + total_refl: int + unique_refl: int + comments: Optional[constr(max_length=200)] = None + # Define attributes for Results here - pass + class Config: + from_attributes = True class ContactCreate(BaseModel): @@ -672,7 +692,8 @@ class SetTellPositionRequest(BaseModel): tell: str pucks: List[SetTellPosition] - from_attributes: ClassVar[bool] = True + class Config: + from_attributes = True class PuckWithTellPosition(BaseModel): diff --git a/logistics/package-lock.json b/logistics/package-lock.json index 6144ab8..065bea1 100644 --- a/logistics/package-lock.json +++ b/logistics/package-lock.json @@ -16,6 +16,7 @@ "date-fns": "^4.1.0", "moment": "^2.30.1", "react": "^18.3.1", + "react-data-grid": "^7.0.0-beta.47", "react-dom": "^18.3.1", "react-qr-code": "^2.0.15", "react-qr-scanner": "^1.0.0-alpha.11", @@ -987,18 +988,32 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.2.tgz", - "integrity": "sha512-CXtq5nR4Su+2I47WPOlWud98Y5Lv8Kyxp2ukhgFx/eW6Blm18VXJO5WuQylPugRo8nbluoi6GvvxBLqHcvqUUw==", + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.5.tgz", + "integrity": "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==", "dev": true, "license": "Apache-2.0", "dependencies": { + "@eslint/core": "^0.10.0", "levn": "^0.4.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz", + "integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -3363,9 +3378,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "funding": [ { "type": "github", @@ -3646,6 +3661,19 @@ "node": ">=0.10.0" } }, + "node_modules/react-data-grid": { + "version": "7.0.0-beta.47", + "resolved": "https://registry.npmjs.org/react-data-grid/-/react-data-grid-7.0.0-beta.47.tgz", + "integrity": "sha512-28kjsmwQGD/9RXYC50zn5Zv/SQMhBBoSvG5seq0fM8XXi9TZ0zr9Z5T3YJqLwcEtoNzTOq3y0njkmdujGkIwQQ==", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0", + "react-dom": "^18.0 || ^19.0" + } + }, "node_modules/react-dom": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", @@ -4136,9 +4164,9 @@ } }, "node_modules/vite": { - "version": "5.4.11", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz", - "integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==", + "version": "5.4.14", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.14.tgz", + "integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==", "license": "MIT", "dependencies": { "esbuild": "^0.21.3", diff --git a/logistics/package.json b/logistics/package.json index a48756f..da3e312 100644 --- a/logistics/package.json +++ b/logistics/package.json @@ -21,6 +21,7 @@ "date-fns": "^4.1.0", "moment": "^2.30.1", "react": "^18.3.1", + "react-data-grid": "^7.0.0-beta.47", "react-dom": "^18.3.1", "react-qr-code": "^2.0.15", "react-qr-scanner": "^1.0.0-alpha.11", diff --git a/logistics/src/pages/DewarStatusTab.tsx b/logistics/src/pages/DewarStatusTab.tsx new file mode 100644 index 0000000..00b5554 --- /dev/null +++ b/logistics/src/pages/DewarStatusTab.tsx @@ -0,0 +1,97 @@ +import React, { useEffect, useState } from "react"; +import DataGrid from "react-data-grid"; +import { Box, Typography, Snackbar, Alert, CircularProgress } from "@mui/material"; +import { LogisticsService } from "../../../frontend/openapi"; + +interface Dewar { + id: string; + dewar_name: string; + status: string; + location: string; + timestamp: string; // You can change this type based on your API response +} + +const DewarStatusTab: React.FC = () => { + const [dewars, setDewars] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const columns = [ + { key: "dewar_name", name: "Dewar Name", resizable: true }, + { + key: "status", + name: "Status", + editable: true, + resizable: true, + editor: (props: { row: any; column: any; onRowChange: any }) => { + return ( + props.onRowChange({ ...props.row, [props.column.key]: e.target.value })} + style={{ + border: "none", + outline: "none", + padding: "4px", + }} + /> + ); + }, + }, + { key: "location", name: "Location", resizable: true }, + { key: "timestamp", name: "Last Updated", resizable: true }, + ]; + + // Fetch dewars when component mounts + useEffect(() => { + fetchDewarData(); + }, []); + + const fetchDewarData = async () => { + setLoading(true); + try { + const dewarData = await LogisticsService.getAllDewarsLogisticsDewarsGet(); // Use your real API call + setDewars(dewarData); + } catch (e) { + setError("Failed to fetch dewar data"); + } finally { + setLoading(false); + } + }; + + const onRowsChange = async (updatedRow: Dewar[]) => { + setDewars(updatedRow); + try { + const updatedDewar = updatedRow[updatedRow.length - 1]; // Get the last edited row + await LogisticsService.updateDewarStatus({ ...updatedDewar }); // Mock API update + } catch (err) { + setError("Error updating dewar"); + } + }; + + return ( + + + Dewar Status + + {loading ? ( + + ) : error ? ( + setError(null)}> + setError(null)}> + {error} + + + ) : ( + + )} + + ); +}; + +export default DewarStatusTab; \ No newline at end of file diff --git a/logistics/src/pages/LogisticsTrackingTab.tsx b/logistics/src/pages/LogisticsTrackingTab.tsx new file mode 100644 index 0000000..a7b2eb6 --- /dev/null +++ b/logistics/src/pages/LogisticsTrackingTab.tsx @@ -0,0 +1,558 @@ +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(null); + const [locationQr, setLocationQr] = useState(null); + const [transactionType, setTransactionType] = useState(""); + const [isModalOpen, setIsModalOpen] = useState(false); + const [selectedSlot, setSelectedSlot] = useState(null); + const [sslError, setSslError] = useState(false); + const [warningMessage, setWarningMessage] = useState(null); + const [selectedSlotData, setSelectedSlotData] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [slotsData, setSlotsData] = useState([]); + + 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(); + + // 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(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 ( + + + Logistics Tracking + + + + + + Dewar QR Code + setDewarQr(e.target.value)} + sx={{ mb: 2 }} + /> + setIsModalOpen(true)} sx={{ fontSize: 40, mb: 2 }}> + + + + Selected Slot + setLocationQr(e.target.value)} + sx={{ mb: 2 }} + disabled + /> + + + + + + + + + + + {selectedSlotData ? ( + + {selectedSlotData ? ( + + {selectedSlotData.label} + {`Shipment: ${selectedSlotData.shipment_name}`} + {`Dewar: ${selectedSlotData.dewar_name || 'N/A'}`} + {`Contact Person: ${selectedSlotData.contact}`} + {`QR Code: ${selectedSlotData.qr_code}`} + {`Occupied: ${selectedSlotData.occupied ? 'Yes' : 'No'}`} + {`Needs Refill: ${selectedSlotData.needsRefillWarning ? 'Yes' : 'No'}`} + {`Time Until Refill: ${selectedSlotData.time_until_refill ?? 'N/A'}`} + {`Last Event: ${formatTimestamp(selectedSlotData.retrievedTimestamp)}`} + {`Local Contact: ${selectedSlotData.local_contact}`} + {`Beamline Location: ${selectedSlotData.beamlineLocation || 'N/A'}`} + + ) : ( + Select a slot to see more details. + )} + + ) : ( + No slot selected + )} + + + + {loading ? ( + Loading... + ) : error ? ( + {error} + ) : ( + + {['X06SA-storage', 'X10SA-storage', 'Novartis-Box'].map((storageKey) => { + const filteredSlots = slotsData.filter((slot) => storageToSlotsMapping[storageKey].includes(slot.qr_code)); + return ( + + ); + })} + + )} + + setIsModalOpen(false)} onScan={handleSlotSelection} /> + + setSslError(false)}> + setSslError(false)} severity="error"> + SSL Error: Unable to establish a secure connection with the server. + + + + {warningMessage && ( + setWarningMessage(null)}> + setWarningMessage(null)}> + {warningMessage} + + + )} + + {error && ( + setError(null)}> + setError(null)}> + {error} + + + )} + + + ); +}; + +export default LogisticsTrackingTab; \ No newline at end of file diff --git a/logistics/src/pages/LogisticsView.tsx b/logistics/src/pages/LogisticsView.tsx index f6a6c6d..90bfbd2 100644 --- a/logistics/src/pages/LogisticsView.tsx +++ b/logistics/src/pages/LogisticsView.tsx @@ -1,535 +1,32 @@ -import React, { useState, useEffect, 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 { LogisticsService, OpenAPI } from '../../../frontend/openapi'; -import type { Dewar, Slot as SlotSchema } from '../../../frontend/openapi/models'; -import styled from 'styled-components'; -import moment from 'moment'; -import { format } from 'date-fns'; - -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; - beamlineLocation?: string; - shipment_name?: string; - contact?: string; - local_contact?: string; -} - -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; -`; +import React, { useState, useEffect } from "react"; +import { Box, Tabs, Tab, Typography } from "@mui/material"; +import LogisticsTrackingTab from "./LogisticsTrackingTab"; +import DewarStatusTab from "./DewarStatusTab"; // Adjust paths as necessary const LogisticsView: React.FC = () => { - const [dewarQr, setDewarQr] = useState(null); - const [locationQr, setLocationQr] = useState(null); - const [transactionType, setTransactionType] = useState(""); - const [isModalOpen, setIsModalOpen] = useState(false); - const [selectedSlot, setSelectedSlot] = useState(null); - const [sslError, setSslError] = useState(false); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [slotsData, setSlotsData] = useState([]); - const [warningMessage, setWarningMessage] = useState(null); - const [selectedSlotData, setSelectedSlotData] = useState(null); - const [retrievedDewar, setRetrievedDewar] = useState(null); + const [currentTab, setCurrentTab] = useState(0); - 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(null); - - 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); - - 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(); - - dewars.forEach((dewar) => { - if (dewar.unique_id) { - dewarMap[dewar.unique_id] = dewar; - console.log(`Dewar ID: ${dewar.unique_id}`); - } - }); - - const newSlotsData = slots.map((slot) => { - let associatedDewar: Dewar | undefined; - - if (slot.dewar_unique_id) { - if (usedDewarUniqueIds.has(slot.dewar_unique_id)) { - const existingSlotId = usedDewarUniqueIds.get(slot.dewar_unique_id); - console.warn(`Dewar with unique ID ${slot.dewar_unique_id} is already assigned to slot ${existingSlotId}`); - setWarningMessage(`Dewar ${slot.dewar_unique_id} is already assigned to slot ${existingSlotId}`); - return { - ...slot, - occupied: false, - dewar: null, - time_until_refill: undefined, - needsRefillWarning: true, - beamlineLocation: undefined, - shipmnet_name: undefined, - contact: undefined, - local_contact: undefined, - }; - } else { - associatedDewar = dewarMap[slot.dewar_unique_id]; - if (associatedDewar) { - usedDewarUniqueIds.set(slot.dewar_unique_id, slot.id); - } - } - } - - return { - ...slot, - occupied: !!associatedDewar, - dewar: associatedDewar || null, - dewar_name: associatedDewar ? associatedDewar.dewar_name : undefined, - needsRefillWarning: !associatedDewar || slot.time_until_refill === undefined, - beamlineLocation: slot.beamlineLocation, - shipment_name: slot.shipment_name, - contact: slot.contact, - local_contact: slot.local_contact, - }; - }); - - // SORT THE SLOTS BY QR CODE - newSlotsData.sort((a, b) => a.qr_code.localeCompare(b.qr_code)); - - setSlotsData(newSlotsData); - } catch (e) { - console.error(e); - setError('Failed to fetch data.'); - } finally { - setLoading(false); - } - }; - - useEffect(() => { - fetchDewarsAndSlots(); - }, []); - - const handleSlotSelection = async (data: { text: string }) => { - const scannedText = data.text.trim(); - - if (beamlineQRCodes.includes(scannedText)) { - if (dewarQr || selectedSlot) { - const dewarId = dewarQr || slotsData.find(slot => slot.qr_code === selectedSlot)?.dewar?.unique_id; - if (dewarId) { - try { - const timestamp = moment().toISOString(); - await LogisticsService.scanDewarLogisticsDewarScanPost({ - dewar_qr_code: dewarId, - location_qr_code: scannedText, - transaction_type: 'beamline', - timestamp: timestamp, - }); - - fetchDewarsAndSlots(); - setWarningMessage(`Dewar ${dewarId} assigned to beamline.`); - } catch (e) { - console.error(e); - setError('Error updating dewar status at beamline.'); - } - } - } else { - fetchDewarAndAssociate(scannedText); - } - return; - } - - if (outgoingQRCodes.includes(scannedText)) { - setDewarQr(scannedText); - handleOutgoing(); - return; - } - - if (slotQRCodes.includes(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); - } - } else { - fetchDewarAndAssociate(scannedText); - } - }; - - 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'); - } + const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { + setCurrentTab(newValue); }; return ( - Logistics Tracking + Logistics Management - - - - - Dewar QR Code - setDewarQr(e.target.value)} - sx={{ mb: 2 }} - /> - setIsModalOpen(true)} sx={{ fontSize: 40, mb: 2 }}> - - - - Selected Slot - setLocationQr(e.target.value)} - sx={{ mb: 2 }} - disabled - /> - - - - - - - - - - - {selectedSlotData ? ( - - {selectedSlotData ? ( - - {selectedSlotData.label} - {`Shipment: ${selectedSlotData.shipment_name}`} - {`Dewar: ${selectedSlotData.dewar_name || 'N/A'}`} - {`Contact Person: ${selectedSlotData.contact}`} - {`QR Code: ${selectedSlotData.qr_code}`} - {`Occupied: ${selectedSlotData.occupied ? 'Yes' : 'No'}`} - {`Needs Refill: ${selectedSlotData.needsRefillWarning ? 'Yes' : 'No'}`} - {`Time Until Refill: ${selectedSlotData.time_until_refill ?? 'N/A'}`} - {`Last Event: ${formatTimestamp(selectedSlotData.retrievedTimestamp)}`} - {`Local Contact: ${selectedSlotData.local_contact}`} - {`Beamline Location: ${selectedSlotData.beamlineLocation || 'N/A'}`} - - ) : ( - Select a slot to see more details. - )} - - ) : ( - No slot selected - )} - - - - {loading ? ( - Loading... - ) : error ? ( - {error} - ) : ( - - {['X06SA-storage', 'X10SA-storage', 'Novartis-Box'].map((storageKey) => { - const filteredSlots = slotsData.filter((slot) => storageToSlotsMapping[storageKey].includes(slot.qr_code)); - return ( - - ); - })} - - )} - - setIsModalOpen(false)} onScan={handleSlotSelection} /> - - setSslError(false)}> - setSslError(false)} severity="error"> - SSL Error: Unable to establish a secure connection with the server. - - - - {warningMessage && ( - setWarningMessage(null)}> - setWarningMessage(null)}> - {warningMessage} - - - )} - - {error && ( - setError(null)}> - setError(null)}> - {error} - - - )} - - ); };