added tabs to logistics frontend
This commit is contained in:
parent
9e5ae2b43c
commit
25673ae05c
@ -214,3 +214,26 @@ class PuckEvent(Base):
|
|||||||
timestamp = Column(DateTime, default=datetime.now)
|
timestamp = Column(DateTime, default=datetime.now)
|
||||||
|
|
||||||
puck = relationship("Puck", back_populates="events")
|
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
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from typing import List, Optional, ClassVar
|
from typing import List, Optional
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pydantic import BaseModel, EmailStr, constr, Field, field_validator
|
from pydantic import BaseModel, EmailStr, constr, Field, field_validator
|
||||||
from datetime import date
|
from datetime import date
|
||||||
@ -362,8 +362,28 @@ class SampleEventResponse(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class Results(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
|
# Define attributes for Results here
|
||||||
pass
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
class ContactCreate(BaseModel):
|
class ContactCreate(BaseModel):
|
||||||
@ -672,7 +692,8 @@ class SetTellPositionRequest(BaseModel):
|
|||||||
tell: str
|
tell: str
|
||||||
pucks: List[SetTellPosition]
|
pucks: List[SetTellPosition]
|
||||||
|
|
||||||
from_attributes: ClassVar[bool] = True
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
class PuckWithTellPosition(BaseModel):
|
class PuckWithTellPosition(BaseModel):
|
||||||
|
46
logistics/package-lock.json
generated
46
logistics/package-lock.json
generated
@ -16,6 +16,7 @@
|
|||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
|
"react-data-grid": "^7.0.0-beta.47",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-qr-code": "^2.0.15",
|
"react-qr-code": "^2.0.15",
|
||||||
"react-qr-scanner": "^1.0.0-alpha.11",
|
"react-qr-scanner": "^1.0.0-alpha.11",
|
||||||
@ -987,18 +988,32 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/plugin-kit": {
|
"node_modules/@eslint/plugin-kit": {
|
||||||
"version": "0.2.2",
|
"version": "0.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.5.tgz",
|
||||||
"integrity": "sha512-CXtq5nR4Su+2I47WPOlWud98Y5Lv8Kyxp2ukhgFx/eW6Blm18VXJO5WuQylPugRo8nbluoi6GvvxBLqHcvqUUw==",
|
"integrity": "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@eslint/core": "^0.10.0",
|
||||||
"levn": "^0.4.1"
|
"levn": "^0.4.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"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": {
|
"node_modules/@humanfs/core": {
|
||||||
"version": "0.19.1",
|
"version": "0.19.1",
|
||||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||||
@ -3363,9 +3378,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.7",
|
"version": "3.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
|
||||||
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
|
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@ -3646,6 +3661,19 @@
|
|||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/react-dom": {
|
||||||
"version": "18.3.1",
|
"version": "18.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||||
@ -4136,9 +4164,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "5.4.11",
|
"version": "5.4.14",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.14.tgz",
|
||||||
"integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==",
|
"integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.21.3",
|
"esbuild": "^0.21.3",
|
||||||
|
@ -21,6 +21,7 @@
|
|||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
|
"react-data-grid": "^7.0.0-beta.47",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-qr-code": "^2.0.15",
|
"react-qr-code": "^2.0.15",
|
||||||
"react-qr-scanner": "^1.0.0-alpha.11",
|
"react-qr-scanner": "^1.0.0-alpha.11",
|
||||||
|
97
logistics/src/pages/DewarStatusTab.tsx
Normal file
97
logistics/src/pages/DewarStatusTab.tsx
Normal file
@ -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<Dewar[]>([]);
|
||||||
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
const [error, setError] = useState<string | null>(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 (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={props.row[props.column.key]}
|
||||||
|
onChange={(e) => 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 (
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h5" gutterBottom>
|
||||||
|
Dewar Status
|
||||||
|
</Typography>
|
||||||
|
{loading ? (
|
||||||
|
<CircularProgress />
|
||||||
|
) : error ? (
|
||||||
|
<Snackbar open autoHideDuration={6000} onClose={() => setError(null)}>
|
||||||
|
<Alert severity="error" onClose={() => setError(null)}>
|
||||||
|
{error}
|
||||||
|
</Alert>
|
||||||
|
</Snackbar>
|
||||||
|
) : (
|
||||||
|
<DataGrid
|
||||||
|
columns={columns}
|
||||||
|
rows={dewars}
|
||||||
|
onRowsChange={onRowsChange}
|
||||||
|
style={{ height: 600, width: "100%" }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DewarStatusTab;
|
558
logistics/src/pages/LogisticsTrackingTab.tsx
Normal file
558
logistics/src/pages/LogisticsTrackingTab.tsx
Normal file
@ -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<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;
|
@ -1,535 +1,32 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect } from "react";
|
||||||
import { Box, Button, TextField, Typography, Grid, IconButton, Snackbar, Alert } from '@mui/material';
|
import { Box, Tabs, Tab, Typography } from "@mui/material";
|
||||||
import { CameraAlt } from '@mui/icons-material';
|
import LogisticsTrackingTab from "./LogisticsTrackingTab";
|
||||||
import ScannerModal from '../components/ScannerModal';
|
import DewarStatusTab from "./DewarStatusTab"; // Adjust paths as necessary
|
||||||
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;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const LogisticsView: React.FC = () => {
|
const LogisticsView: React.FC = () => {
|
||||||
const [dewarQr, setDewarQr] = useState<string | null>(null);
|
const [currentTab, setCurrentTab] = useState(0);
|
||||||
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 [loading, setLoading] = useState<boolean>(true);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const [slotsData, setSlotsData] = useState<SlotData[]>([]);
|
|
||||||
const [warningMessage, setWarningMessage] = useState<string | null>(null);
|
|
||||||
const [selectedSlotData, setSelectedSlotData] = useState<SlotData | null>(null);
|
|
||||||
const [retrievedDewar, setRetrievedDewar] = useState<string | null>(null);
|
|
||||||
|
|
||||||
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 handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
|
||||||
const audioRef = useRef<HTMLAudioElement | null>(null);
|
setCurrentTab(newValue);
|
||||||
|
|
||||||
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<string, string>();
|
|
||||||
|
|
||||||
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');
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Typography variant="h4" gutterBottom>
|
<Typography variant="h4" gutterBottom>
|
||||||
Logistics Tracking
|
Logistics Management
|
||||||
</Typography>
|
</Typography>
|
||||||
|
<Tabs value={currentTab} onChange={handleTabChange}>
|
||||||
<Grid container spacing={2}>
|
<Tab label="Logistics Tracking" />
|
||||||
<Grid item xs={12} sm={6}>
|
<Tab label="Dewar Status Table" />
|
||||||
<Box display="flex" flexDirection="column" alignItems="flex-start">
|
</Tabs>
|
||||||
<Typography variant="h6">Dewar QR Code</Typography>
|
<Box hidden={currentTab !== 0}>
|
||||||
<TextField
|
<LogisticsTrackingTab // Pass the warningMessage down
|
||||||
label="Enter Dewar QR Code"
|
/>
|
||||||
variant="outlined"
|
</Box>
|
||||||
fullWidth
|
<Box hidden={currentTab !== 1}>
|
||||||
value={dewarQr ?? ''}
|
<DewarStatusTab />
|
||||||
onChange={(e) => setDewarQr(e.target.value)}
|
</Box>
|
||||||
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>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user