https and ssl integration on the backend, frontend and started integration of logistics app as a separate frontend

This commit is contained in:
GotthardG
2024-11-19 09:56:05 +01:00
parent a91d74b718
commit a931bfb8ec
14 changed files with 264 additions and 267 deletions

View File

View File

@ -1 +1,2 @@
from .data import contacts, return_addresses, dewars, proposals, shipments, pucks, samples, dewar_types, serial_numbers
from .slots_data import slots

View File

@ -1,7 +1,8 @@
from app.models import ContactPerson, Address, Dewar, Proposal, Shipment, Puck, Sample, DewarType, DewarSerialNumber
from app.models import ContactPerson, Address, Dewar, Proposal, Shipment, Puck, Sample, DewarType, DewarSerialNumber, Slot
from datetime import datetime
import random
import uuid
import time
import hashlib
dewar_types = [
@ -44,8 +45,14 @@ return_addresses = [
]
# Utilize a function to generate unique IDs
def generate_unique_id():
return str(uuid.uuid4())
def generate_unique_id(length=16):
base_string = f"{time.time()}{random.randint(0, 10 ** 6)}"
hash_object = hashlib.sha256(base_string.encode())
hash_digest = hash_object.hexdigest()
short_unique_id = ''.join(random.choices(hash_digest, k=length))
return short_unique_id
# Define dewars with unique IDs
dewars = [
@ -54,20 +61,20 @@ dewars = [
dewar_serial_number_id=2, tracking_number='TRACK123',
return_address_id=1, contact_person_id=1, status='Ready for Shipping',
ready_date=datetime.strptime('2023-09-30', '%Y-%m-%d'), shipping_date=None, arrival_date=None,
returning_date=None, qrcode=generate_unique_id()
returning_date=None, unique_id=generate_unique_id()
),
Dewar(
id=2, dewar_name='Dewar Two', dewar_type_id=3,
dewar_serial_number_id=1, tracking_number='TRACK124',
return_address_id=2, contact_person_id=2, status='In Preparation',
ready_date=None, shipping_date=None, arrival_date=None, returning_date=None, qrcode=generate_unique_id()
ready_date=None, shipping_date=None, arrival_date=None, returning_date=None, unique_id=generate_unique_id()
),
Dewar(
id=3, dewar_name='Dewar Three', dewar_type_id=2,
dewar_serial_number_id=3, tracking_number='TRACK125',
return_address_id=1, contact_person_id=3, status='Not Shipped',
ready_date=datetime.strptime('2024-01-01', '%Y-%m-%d'), shipping_date=None, arrival_date=None,
returning_date=None, qrcode=''
returning_date=None, unique_id=None
),
Dewar(
id=4, dewar_name='Dewar Four', dewar_type_id=2,
@ -75,7 +82,7 @@ dewars = [
return_address_id=1, contact_person_id=3, status='Delayed',
ready_date=datetime.strptime('2024-01-01', '%Y-%m-%d'),
shipping_date=datetime.strptime('2024-01-02', '%Y-%m-%d'),
arrival_date=None, returning_date=None, qrcode=''
arrival_date=None, returning_date=None, unique_id=None
),
Dewar(
id=5, dewar_name='Dewar Five', dewar_type_id=1,
@ -83,7 +90,7 @@ dewars = [
return_address_id=1, contact_person_id=3, status='Returned',
arrival_date=datetime.strptime('2024-01-03', '%Y-%m-%d'),
returning_date=datetime.strptime('2024-01-07', '%Y-%m-%d'),
qrcode=''
unique_id=None
),
]

View File

@ -0,0 +1,36 @@
from datetime import datetime, timedelta
from app.models import Slot
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",
"X10SA-beamline", "X06SA-beamline", "X06DA-beamline",
"X10SA-outgoing", "X06-outgoing"
]
def timedelta_to_str(td: timedelta) -> str:
days, seconds = td.days, td.seconds
hours = days * 24 + seconds // 3600
minutes = (seconds % 3600) // 60
return f'PT{hours}H{minutes}M'
slots = [
Slot(
id=str(i + 1), # Convert id to string to match your schema
qr_code=qrcode,
label=qrcode.split('-')[0],
qr_base=qrcode.split('-')[1] if '-' in qrcode else '',
occupied=False,
needs_refill=False,
last_refill=datetime.utcnow(),
time_until_refill=timedelta_to_str(timedelta(hours=24)) # Serialize timedelta to ISO 8601 string
)
for i, qrcode in enumerate(slotQRCodes)
]

View File

@ -1,7 +1,9 @@
# database.py
from sqlalchemy.orm import Session
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from app import models
SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db" # Use appropriate path or database URL
@ -10,7 +12,6 @@ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
# Dependency
def get_db():
db = SessionLocal()
@ -19,22 +20,18 @@ def get_db():
finally:
db.close()
def init_db():
# Import models inside function to avoid circular dependency
from app import models
Base.metadata.create_all(bind=engine)
def load_sample_data(session: Session):
# Import models inside function to avoid circular dependency
from app.data import contacts, return_addresses, dewars, proposals, shipments, pucks, samples, dewar_types, serial_numbers
from app import models
from app.data import contacts, return_addresses, dewars, proposals, shipments, pucks, samples, dewar_types, serial_numbers, slots
# If any data already exists, skip seeding
if session.query(models.ContactPerson).first():
return
session.add_all(contacts + return_addresses + dewars + proposals + shipments + pucks + samples + dewar_types + serial_numbers)
session.commit()
session.add_all(contacts + return_addresses + dewars + proposals + shipments + pucks + samples + dewar_types + serial_numbers + slots)
session.commit()

View File

@ -73,13 +73,13 @@ class Dewar(Base):
shipping_date = Column(Date, nullable=True)
arrival_date = Column(Date, nullable=True)
returning_date = Column(Date, nullable=True)
unique_id = Column(String(36), default=lambda: str(uuid.uuid4()), unique=True, index=True, nullable=True)
qrcode = Column(String, nullable=True)
unique_id = Column(String, unique=True, index=True, nullable=True)
shipment_id = Column(Integer, ForeignKey("shipments.id"))
return_address_id = Column(Integer, ForeignKey("addresses.id"))
contact_person_id = Column(Integer, ForeignKey("contact_persons.id"))
shipment = relationship("Shipment", back_populates="dewars")
events = relationship("LogisticsEvent", back_populates="dewar")
return_address = relationship("Address")
contact_person = relationship("ContactPerson")
pucks = relationship("Puck", back_populates="dewar")
@ -132,22 +132,30 @@ class Sample(Base):
puck_id = Column(Integer, ForeignKey('pucks.id'))
puck = relationship("Puck", back_populates="samples")
class Slot(Base):
__tablename__ = "slots"
id = Column(String, primary_key=True, index=True)
qr_code = Column(String, unique=True, index=True)
label = Column(String)
qr_base = Column(String, nullable=True)
occupied = Column(Boolean, default=False)
needs_refill = Column(Boolean, default=False)
time_until_refill = Column(Interval, nullable=True)
last_refill = Column(DateTime, default=datetime.utcnow)
time_until_refill = Column(Integer) # store as total seconds
@property
def calculate_time_until_refill(self):
if self.last_refill and self.time_until_refill:
return self.last_refill + self.time_until_refill - datetime.utcnow()
return None
class LogisticsEvent(Base):
__tablename__ = "logistics_events"
__tablename__ = 'logistics_events'
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
dewar_id = Column(Integer, ForeignKey('dewars.id'), nullable=False)
slot_id = Column(String, ForeignKey('slots.id'), nullable=True)
event_type = Column(String, nullable=False)
timestamp = Column(DateTime, default=datetime.utcnow)
dewar = relationship("Dewar")
slot = relationship("Slot")
dewar = relationship("Dewar", back_populates="events")

View File

@ -1,6 +1,4 @@
import os
import tempfile # <-- Add this import
import xml.etree.ElementTree as ET
import os, tempfile, time, random, hashlib
from fastapi import APIRouter, HTTPException, status, Depends, Response
from sqlalchemy.orm import Session, joinedload
from typing import List
@ -38,9 +36,12 @@ from app.crud import get_shipments, get_shipment_by_id # Import CRUD functions
router = APIRouter()
def generate_unique_id(db: Session) -> str:
def generate_unique_id(db: Session, length: int = 16) -> str:
while True:
unique_id = str(uuid.uuid4())
base_string = f"{time.time()}{random.randint(0, 10 ** 6)}"
hash_object = hashlib.sha256(base_string.encode())
hash_digest = hash_object.hexdigest()
unique_id = ''.join(random.choices(hash_digest, k=length))
existing_dewar = db.query(DewarModel).filter(DewarModel.unique_id == unique_id).first()
if not existing_dewar:
break

View File

@ -1,107 +1,113 @@
from fastapi import APIRouter, HTTPException, Depends
from sqlalchemy.orm import Session
import logging
from datetime import datetime, timedelta
from typing import List
from app.models import Dewar as DewarModel, Slot as SlotModel, LogisticsEvent as LogisticsEventModel
from app.schemas import LogisticsEventCreate, SlotCreate, Slot as SlotSchema, Dewar as DewarSchema
from app.schemas import LogisticsEventCreate, Slot as SlotSchema, Dewar as DewarSchema
from app.database import get_db
from app.data import slots_data
import logging
router = APIRouter()
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@router.get("/dewars", response_model=List[DewarSchema])
async def get_all_prouts(db: Session = Depends(get_db)):
try:
dewars = db.query(DewarModel).all()
logging.info(f"Retrieved {len(dewars)} dewars from the database")
return dewars
except Exception as e:
logger.error(f"An error occurred: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
async def get_all_dewars(db: Session = Depends(get_db)):
dewars = db.query(DewarModel).all()
return dewars
@router.get("/dewar/{qr_code}", response_model=DewarSchema)
async def get_dewar_by_qr_code(qr_code: str, db: Session = Depends(get_db)):
logger.info(f"Received qr_code: {qr_code}")
trimmed_qr_code = qr_code.strip()
logger.info(f"Trimmed qr_code after stripping: {trimmed_qr_code}")
@router.get("/dewar/{unique_id}", response_model=DewarSchema)
async def get_dewar_by_unique_id(unique_id: str, db: Session = Depends(get_db)):
dewar = db.query(DewarModel).filter(DewarModel.unique_id == unique_id.strip()).first()
if not dewar:
raise HTTPException(status_code=404, detail="Dewar not found")
return dewar
try:
dewar = db.query(DewarModel).filter(DewarModel.unique_id == trimmed_qr_code).first()
logger.info(f"Query Result: {dewar}")
if dewar:
return dewar
else:
logger.error(f"Dewar not found for unique_id: {trimmed_qr_code}")
raise HTTPException(status_code=404, detail="Dewar not found")
except Exception as e:
logger.error(f"An error occurred: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@router.post("/dewar/scan", response_model=LogisticsEventCreate)
@router.post("/dewar/scan", response_model=dict)
async def scan_dewar(event_data: LogisticsEventCreate, db: Session = Depends(get_db)):
dewar_qr_code = event_data.dewar_qr_code
location_qr_code = event_data.location_qr_code
transaction_type = event_data.transaction_type
try:
dewar = db.query(DewarModel).filter(DewarModel.unique_id == dewar_qr_code).first()
if not dewar:
raise HTTPException(status_code=404, detail="Dewar not found")
dewar = db.query(DewarModel).filter(DewarModel.unique_id == dewar_qr_code).first()
if not dewar:
raise HTTPException(status_code=404, detail="Dewar not found")
if transaction_type == 'incoming':
slot = db.query(SlotModel).filter(SlotModel.id == location_qr_code).first()
if not slot or slot.occupied:
raise HTTPException(status_code=404, detail="Slot not found or already occupied")
slot.occupied = True
log_event(db, dewar.id, slot.id, 'incoming')
slot = db.query(SlotModel).filter(SlotModel.qr_code == location_qr_code).first()
if transaction_type == 'incoming':
if not slot or slot.occupied:
raise HTTPException(status_code=400, detail="Slot not found or already occupied")
slot.occupied = True
elif transaction_type == 'beamline':
log_event(db, dewar.id, None, 'beamline')
elif transaction_type == 'outgoing':
if not slot or not slot.occupied:
raise HTTPException(status_code=400, detail="Slot not found or not occupied")
slot.occupied = False
elif transaction_type == 'outgoing':
slot = db.query(SlotModel).filter(SlotModel.id == location_qr_code).first()
if not slot or not slot.occupied:
raise HTTPException(status_code=404, detail="Slot not found or not occupied")
slot.occupied = False
log_event(db, dewar.id, slot.id, 'outgoing')
# Log the event
log_event(db, dewar.id, slot.id if slot else None, transaction_type)
elif transaction_type == 'release':
slot = db.query(SlotModel).filter(SlotModel.id == location_qr_code).first()
if not slot or not slot.occupied:
raise HTTPException(status_code=404, detail="Slot not found or not occupied")
slot.occupied = False
log_event(db, dewar.id, slot.id, 'released')
db.commit()
return {"message": "Status updated successfully"}
db.commit()
logger.info(f"Status updated successfully for Dewar ID: {dewar.id}")
return {"message": "Status updated successfully"}
except Exception as e:
logger.error(f"An error occurred: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
def log_event(db: Session, dewar_id: int, slot_id: int, event_type: str):
new_event = LogisticsEventModel(dewar_id=dewar_id if dewar_id else None, slot_id=slot_id, event_type=event_type)
def log_event(db: Session, dewar_id: int, slot_id: str, event_type: str):
new_event = LogisticsEventModel(dewar_id=dewar_id, slot_id=slot_id, event_type=event_type)
db.add(new_event)
db.commit()
logger.info(f"Logged event {event_type} for Dewar ID: {dewar_id}")
@router.get("/slots/refill-status", response_model=List[SlotSchema])
# Convert SQLAlchemy model to dictionary (if necessary)
def slot_to_dict(slot: SlotModel) -> dict:
return {
"id": slot.id,
"qr_code": slot.qr_code,
"label": slot.label,
"qr_base": slot.qr_base,
"occupied": slot.occupied,
"needs_refill": slot.needs_refill,
"last_refill": slot.last_refill.isoformat(),
"time_until_refill": str(slot.time_until_refill) if slot.time_until_refill else None # Ensure correct format
}
@router.get("/slots", response_model=List[dict])
def read_slots(db: Session = Depends(get_db)):
return [slot_to_dict(slot) for slot in db.query(SlotModel).all()]
@router.get("/dewars/refill-status", response_model=List[DewarSchema])
async def refill_status(db: Session = Depends(get_db)):
slots_needing_refill = db.query(SlotModel).filter(SlotModel.needs_refill == True).all()
dewars = db.query(DewarModel).all()
result = []
current_time = datetime.utcnow()
for slot in slots_needing_refill:
time_until_next_refill = slot.last_refill + timedelta(hours=24) - current_time
result.append({
'slot_id': slot.id,
'needs_refill': slot.needs_refill,
'time_until_refill': str(time_until_next_refill)
})
for dewar in dewars:
last_refill_event = (
db.query(LogisticsEventModel)
.filter(
LogisticsEventModel.dewar_id == dewar.id,
LogisticsEventModel.event_type == 'refill'
)
.order_by(LogisticsEventModel.timestamp.desc())
.first()
)
time_until_refill = None
if last_refill_event:
time_until_refill = last_refill_event.timestamp + timedelta(hours=24) - current_time
dewar_data = DewarSchema(
id=dewar.id,
dewar_name=dewar.dewar_name,
unique_id=dewar.unique_id,
time_until_refill=str(time_until_refill) if time_until_refill else None # Ensure correct format
)
result.append(dewar_data)
return result

View File

@ -184,7 +184,6 @@ class DewarBase(BaseModel):
shipping_date: Optional[date]
arrival_date: Optional[date]
returning_date: Optional[date]
qrcode: str
contact_person_id: Optional[int]
return_address_id: Optional[int]
pucks: List[PuckCreate] = []
@ -216,7 +215,6 @@ class DewarUpdate(BaseModel):
shipping_date: Optional[date] = None
arrival_date: Optional[date] = None
returning_date: Optional[date] = None
qrcode: Optional[str] = None
contact_person_id: Optional[int] = None
address_id: Optional[int] = None
@ -273,20 +271,20 @@ class ShipmentCreate(BaseModel):
class UpdateShipmentComments(BaseModel):
comments: str
class LogisticsEventCreate(BaseModel):
dewar_qr_code: str
location_qr_code: str
transaction_type: str
class SlotCreate(BaseModel):
id: int
class Slot(BaseModel):
id: str
qr_code: str
label: str
qr_base: str
occupied: bool
needs_refill: bool
last_refill: datetime
occupied: bool
class Slot(BaseModel):
slot_id: int
needs_refill: bool
time_until_refill: str
class Config:

View File

@ -1,14 +1,16 @@
// fetch-and-generate-openapi.js
import fs from 'fs';
import http from 'http';
import https from 'https'; // Use https instead of http
import { exec } from 'child_process';
import chokidar from 'chokidar';
import path from 'path';
import util from 'util';
const OPENAPI_URL = 'http://127.0.0.1:8000/openapi.json';
const OPENAPI_URL = 'https://127.0.0.1:8000/openapi.json';
const SCHEMA_PATH = path.resolve('./src/openapi.json');
const OUTPUT_DIRECTORY = path.resolve('./openapi');
const SSL_KEY_PATH = path.resolve('../backend/ssl/key.pem'); // Path to SSL key
const SSL_CERT_PATH = path.resolve('../backend/ssl/cert.pem'); // Path to SSL certificate
console.log(`Using SCHEMA_PATH: ${SCHEMA_PATH}`);
console.log(`Using OUTPUT_DIRECTORY: ${OUTPUT_DIRECTORY}`);
@ -38,8 +40,14 @@ async function fetchAndGenerate() {
console.log("🚀 Fetching OpenAPI schema...");
try {
const options = {
rejectUnauthorized: false,
key: fs.readFileSync(SSL_KEY_PATH),
cert: fs.readFileSync(SSL_CERT_PATH),
};
const res = await new Promise((resolve, reject) => {
http.get(OPENAPI_URL, resolve).on('error', reject);
https.get(OPENAPI_URL, options, resolve).on('error', reject);
});
let data = '';
@ -97,7 +105,7 @@ const watcher = chokidar.watch(backendDirectory, { persistent: true, ignored: [S
watcher
.on('add', debounce(fetchAndGenerate, debounceDelay))
.on('change', debounce(fetchAndGenerate, debounceDelay))
.on('change', debounce(fetchAndGenerate, debounceDelay)) // Corrected typo here
.on('unlink', debounce(fetchAndGenerate, debounceDelay));
console.log(`👀 Watching for changes in ${backendDirectory}`);

View File

@ -99,7 +99,7 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
const [knownSerialNumbers, setKnownSerialNumbers] = useState<DewarSerialNumber[]>([]);
const [selectedSerialNumber, setSelectedSerialNumber] = useState<string>('');
const [isQRCodeGenerated, setIsQRCodeGenerated] = useState(false);
const [qrCodeValue, setQrCodeValue] = useState(dewar.qrcode || '');
const [qrCodeValue, setQrCodeValue] = useState(dewar.unique_id || '');
const qrCodeRef = useRef<HTMLCanvasElement>(null); //
useEffect(() => {
@ -368,7 +368,6 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
shipping_date: formatDate(dewar.shipping_date),
arrival_date: dewar.arrival_date,
returning_date: dewar.returning_date,
qrcode: dewar.qrcode,
return_address_id: parseInt(selectedReturnAddress ?? '', 10),
contact_person_id: parseInt(selectedContactPerson ?? '', 10),
};
@ -391,13 +390,13 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
setChangesMade(true);
};
const handleGenerateQRCode = async () => {
const handleGenerateQRCode = () => {
if (!dewar) return;
try {
const response = await DewarsService.generateDewarQrcodeDewarsDewarIdGenerateQrcodePost(dewar.id);
setQrCodeValue(response.qrcode); // assuming the backend returns the QR code value
setIsQRCodeGenerated(true); // to track the state if the QR code is generated
const newQrCodeValue = dewar.unique_id; // Using unique_id directly for QR code value
setQrCodeValue(newQrCodeValue);
setIsQRCodeGenerated(true);
setFeedbackMessage("QR Code generated successfully");
setOpenSnackbar(true);
} catch (error) {
@ -510,31 +509,29 @@ const DewarDetails: React.FC<DewarDetailsProps> = ({
)}
</FormControl>
<Box sx={{ marginTop: 2 }}>
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', marginBottom: 2 }}>
{qrCodeValue ? (
<Box sx={{ textAlign: 'center', marginBottom: 2 }}>
<QRCode id="qrCodeCanvas" value={qrCodeValue} size={150} />
<Box sx={{ display: 'flex', alignItems: 'center', marginTop: 1 }}>
<Tooltip title="Download Label">
<IconButton onClick={handleDownloadLabel} sx={{ transform: 'scale(1.5)', margin: 1 }}>
<DownloadIcon />
</IconButton>
</Tooltip>
<Typography variant="body2">Label is ready for download</Typography>
</Box>
<Box sx={{ textAlign: 'center', marginBottom: 2 }}>
{qrCodeValue ? (
<Box>
<QRCode id="qrCodeCanvas" value={qrCodeValue} size={150} />
<Box sx={{ display: 'flex', alignItems: 'center', marginTop: 1 }}>
<Tooltip title="Download Label">
<IconButton onClick={handleDownloadLabel} sx={{ transform: 'scale(1.5)', margin: 1 }}>
<DownloadIcon />
</IconButton>
</Tooltip>
<Typography variant="body2">Label is ready for download</Typography>
</Box>
) : (
<Typography>No QR code available</Typography>
)}
<Button
variant="contained"
sx={{ marginTop: 1 }}
onClick={handleGenerateQRCode}
>
Generate QR Code
</Button>
</Box>
</Box>
) : (
<Typography>No QR code available</Typography>
)}
<Button
variant="contained"
sx={{ marginTop: 1 }}
onClick={handleGenerateQRCode}
>
Generate QR Code
</Button>
</Box>
<Box sx={{ marginTop: 2 }}>

View File

@ -44,7 +44,6 @@ const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
shipping_date: null,
arrival_date: null,
returning_date: null,
qrcode: 'N/A',
contact_person_id: selectedShipment?.contact_person?.id,
return_address_id: selectedShipment?.return_address?.id,
};
@ -275,8 +274,8 @@ const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', marginRight: 2 }}>
{dewar.qrcode ? (
<QRCode value={dewar.qrcode} size={70} />
{dewar.unique_id ? (
<QRCode value={dewar.unique_id} size={70} />
) : (
<Box
sx={{
@ -338,10 +337,10 @@ const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
setTrackingNumber={(value) => {
setLocalSelectedDewar((prev) => (prev ? { ...prev, tracking_number: value as string } : prev));
}}
initialContactPersons={localSelectedDewar?.contact_person ? [localSelectedDewar.contact_person] : []} // Focus on dewar contact person
initialReturnAddresses={localSelectedDewar?.return_address ? [localSelectedDewar.return_address] : []} // Focus on dewar return address
defaultContactPerson={localSelectedDewar?.contact_person ?? undefined} // Use `?? undefined`
defaultReturnAddress={localSelectedDewar?.return_address ?? undefined} // Use `?? undefined`
initialContactPersons={localSelectedDewar?.contact_person ? [localSelectedDewar.contact_person] : []}
initialReturnAddresses={localSelectedDewar?.return_address ? [localSelectedDewar.return_address] : []}
defaultContactPerson={localSelectedDewar?.contact_person ?? undefined}
defaultReturnAddress={localSelectedDewar?.return_address ?? undefined}
shipmentId={selectedShipment?.id ?? null}
refreshShipments={refreshShipments}
/>

View File

@ -1,75 +1,64 @@
import React from 'react';
import { Box } from '@mui/material';
import { QrCode, AcUnit, AccessTime } from '@mui/icons-material';
import { Box, Typography } from '@mui/material';
import styled from 'styled-components';
interface SlotProps {
data: SlotData;
onSelect: (slot: SlotData) => void;
isSelected: boolean;
}
import LocalGasStationIcon from '@mui/icons-material/LocalGasStation'; // Icon for refilling indicator.
export interface SlotData {
id: string;
qr_code: string;
label: string;
qr_base: string;
occupied: boolean;
needsRefill: boolean;
timeUntilRefill: string;
dewar_unique_id?: string; // Optional additional information.
dewar_name?: string; // Optional dewar information.
needs_refill?: boolean; // Indicator for refill requirement.
}
const SlotContainer = styled(Box)<{ isOccupied: boolean, isSelected: boolean }>`
width: 90px;
height: 180px;
margin: 10px;
background-color: ${({ isOccupied }) => (isOccupied ? '#ffebee' : '#e8f5e9')}; /* occupied = light red, free = light green */
border: ${({ isSelected }) => (isSelected ? '3px solid blue' : '2px solid #aaaaaa')};
border-radius: 5px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
interface SlotProps {
data: SlotData;
isSelected: boolean;
onSelect: (slot: SlotData) => void;
}
const StyledSlot = styled(Box)<{ isSelected: boolean; isOccupied: boolean }>`
padding: 16px;
margin: 8px;
width: 150px; // Increase the width to accommodate more info.
height: 150px; // Increase the height to accommodate more info.
background-color: ${({ isSelected, isOccupied }) =>
isSelected ? '#3f51b5' : isOccupied ? '#f44336' : '#4caf50'};
color: white;
cursor: pointer;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
padding: 10px;
box-sizing: border-box;
cursor: pointer;
position: relative;
border-radius: 8px;
box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.1);
transition: transform 0.2s;
&:hover {
transform: scale(1.05);
}
`;
const SlotNumber = styled.div`
font-size: 20px;
font-weight: bold;
`;
const QrCodeIcon = styled(QrCode)`
font-size: 40px;
color: #aaaaaa;
`;
const RefillIcon = styled(AcUnit)`
font-size: 20px;
color: #1e88e5;
margin-top: auto;
`;
const ClockIcon = styled(AccessTime)`
font-size: 20px;
color: #ff6f00;
`;
const Slot: React.FC<SlotProps> = ({ data, onSelect, isSelected }) => {
const { id, occupied, needsRefill, timeUntilRefill } = data;
const Slot: React.FC<SlotProps> = ({ data, isSelected, onSelect }) => {
return (
<SlotContainer isOccupied={occupied} onClick={() => onSelect(data)} isSelected={isSelected}>
<SlotNumber>{id}</SlotNumber>
<QrCodeIcon />
{occupied && (
<>
{needsRefill && <RefillIcon titleAccess="Needs Refill" />}
<ClockIcon titleAccess={`Time until refill: ${timeUntilRefill}`} />
</>
<StyledSlot
isSelected={isSelected}
isOccupied={data.occupied}
onClick={() => onSelect(data)}
>
<Typography variant="h6">{data.label}</Typography>
{data.dewar_name && (
<Typography variant="body2">{`Dewar: ${data.dewar_name}`}</Typography>
)}
</SlotContainer>
{data.dewar_unique_id && (
<Typography variant="body2">{`ID: ${data.dewar_unique_id}`}</Typography>
)}
{data.needs_refill && <LocalGasStationIcon />}
</StyledSlot>
);
}
};
export default Slot;

View File

@ -1,7 +1,7 @@
import React, { useState } from 'react';
import { Box, Typography } from '@mui/material';
import styled from 'styled-components';
import Slot, { SlotData } from './Slots';
import Slot, { SlotData } from '../components/Slots';
const StorageContainer = styled(Box)`
display: flex;
@ -26,91 +26,41 @@ const StorageWrapper = styled.div`
interface StorageProps {
name: string;
selectedSlot: string | null;
slotsData: SlotData[];
onSelectSlot: (slot: SlotData) => void;
}
const storageSlotsData: { [key: string]: SlotData[] } = {
"X06SA-storage": [
{ id: "A1-X06SA", occupied: false, needsRefill: false, timeUntilRefill: '' },
{ id: "A2-X06SA", occupied: true, needsRefill: true, timeUntilRefill: '12h' },
{ id: "A3-X06SA", occupied: true, needsRefill: true, timeUntilRefill: '12h' },
{ id: "A4-X06SA", occupied: true, needsRefill: true, timeUntilRefill: '12h' },
{ id: "A5-X06SA", occupied: true, needsRefill: true, timeUntilRefill: '12h' },
{ id: "B1-X06SA", occupied: false, needsRefill: false, timeUntilRefill: '' },
{ id: "B2-X06SA", occupied: true, needsRefill: true, timeUntilRefill: '12h' },
{ id: "B3-X06SA", occupied: true, needsRefill: true, timeUntilRefill: '12h' },
{ id: "B4-X06SA", occupied: true, needsRefill: true, timeUntilRefill: '12h' },
{ id: "B5-X06SA", occupied: true, needsRefill: true, timeUntilRefill: '12h' },
{ id: "C1-X06SA", occupied: false, needsRefill: false, timeUntilRefill: '' },
{ id: "C2-X06SA", occupied: true, needsRefill: true, timeUntilRefill: '12h' },
{ id: "C3-X06SA", occupied: true, needsRefill: true, timeUntilRefill: '12h' },
{ id: "C4-X06SA", occupied: true, needsRefill: true, timeUntilRefill: '12h' },
{ id: "C5-X06SA", occupied: true, needsRefill: true, timeUntilRefill: '12h' },
{ id: "D1-X06SA", occupied: false, needsRefill: false, timeUntilRefill: '' },
{ id: "D2-X06SA", occupied: true, needsRefill: true, timeUntilRefill: '12h' },
{ id: "D3-X06SA", occupied: true, needsRefill: true, timeUntilRefill: '12h' },
{ id: "D4-X06SA", occupied: true, needsRefill: true, timeUntilRefill: '12h' },
{ id: "D5-X06SA", occupied: true, needsRefill: true, timeUntilRefill: '12h' },
],
"X10SA-storage": [
{ id: "A1-X10SA", occupied: false, needsRefill: false, timeUntilRefill: '' },
{ id: "A2-X10SA", occupied: true, needsRefill: true, timeUntilRefill: '12h' },
{ id: "A3-X10SA", occupied: true, needsRefill: true, timeUntilRefill: '12h' },
{ id: "A4-X10SA", occupied: true, needsRefill: true, timeUntilRefill: '12h' },
{ id: "A5-X10SA", occupied: true, needsRefill: true, timeUntilRefill: '12h' },
{ id: "B1-X10SA", occupied: false, needsRefill: false, timeUntilRefill: '' },
{ id: "B2-X10SA", occupied: true, needsRefill: true, timeUntilRefill: '12h' },
{ id: "B3-X10SA", occupied: true, needsRefill: true, timeUntilRefill: '12h' },
{ id: "B4-X10SA", occupied: true, needsRefill: true, timeUntilRefill: '12h' },
{ id: "B5-X10SA", occupied: true, needsRefill: true, timeUntilRefill: '12h' },
{ id: "C1-X10SA", occupied: false, needsRefill: false, timeUntilRefill: '' },
{ id: "C2-X10SA", occupied: true, needsRefill: true, timeUntilRefill: '12h' },
{ id: "C3-X10SA", occupied: true, needsRefill: true, timeUntilRefill: '12h' },
{ id: "C4-X10SA", occupied: true, needsRefill: true, timeUntilRefill: '12h' },
{ id: "C5-X10SA", occupied: true, needsRefill: true, timeUntilRefill: '12h' },
{ id: "D1-X10SA", occupied: false, needsRefill: false, timeUntilRefill: '' },
{ id: "D2-X10SA", occupied: true, needsRefill: true, timeUntilRefill: '12h' },
{ id: "D3-X10SA", occupied: true, needsRefill: true, timeUntilRefill: '12h' },
{ id: "D4-X10SA", occupied: true, needsRefill: true, timeUntilRefill: '12h' },
{ id: "D5-X10SA", occupied: true, needsRefill: true, timeUntilRefill: '12h' },
],
"Novartis-Box": [
{ id: "NB1", occupied: true, needsRefill: true, timeUntilRefill: '6h' },
{ id: "NB2", occupied: true, needsRefill: true, timeUntilRefill: '6h' },
{ id: "NB3", occupied: true, needsRefill: true, timeUntilRefill: '6h' },
{ id: "NB4", occupied: true, needsRefill: true, timeUntilRefill: '6h' },
{ id: "NB5", occupied: true, needsRefill: true, timeUntilRefill: '6h' },
{ id: "NB6", occupied: true, needsRefill: true, timeUntilRefill: '6h' },
],
};
const Storage: React.FC<StorageProps> = ({ name, selectedSlot }) => {
const Storage: React.FC<StorageProps> = ({ name, selectedSlot, slotsData, onSelectSlot }) => {
const [highlightedSlot, setHighlightedSlot] = useState<SlotData | null>(null);
const handleSlotSelect = (slot: SlotData) => {
setHighlightedSlot(slot);
onSelectSlot(slot);
console.log('Selected slot:', slot);
};
console.log("Rendering Storage Component with name:", name);
return (
<StorageContainer>
<Typography variant="h5">{name} Slots</Typography>
<StorageWrapper>
{storageSlotsData[name].map((slot) => (
{slotsData.map((slot: SlotData) => (
<Slot
key={slot.id}
data={slot}
onSelect={handleSlotSelect}
isSelected={selectedSlot === slot.id}
isSelected={selectedSlot === slot.qr_code}
/>
))}
</StorageWrapper>
{highlightedSlot && (
<Typography variant="subtitle1">
Selected Slot: {highlightedSlot.id}
Selected Slot: {highlightedSlot.label}
</Typography>
)}
</StorageContainer>
);
}
};
export default Storage;