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 .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 from datetime import datetime
import random import random
import uuid import time
import hashlib
dewar_types = [ dewar_types = [
@ -44,8 +45,14 @@ return_addresses = [
] ]
# Utilize a function to generate unique IDs # Utilize a function to generate unique IDs
def generate_unique_id(): def generate_unique_id(length=16):
return 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()
short_unique_id = ''.join(random.choices(hash_digest, k=length))
return short_unique_id
# Define dewars with unique IDs # Define dewars with unique IDs
dewars = [ dewars = [
@ -54,20 +61,20 @@ dewars = [
dewar_serial_number_id=2, tracking_number='TRACK123', dewar_serial_number_id=2, tracking_number='TRACK123',
return_address_id=1, contact_person_id=1, status='Ready for Shipping', 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, 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( Dewar(
id=2, dewar_name='Dewar Two', dewar_type_id=3, id=2, dewar_name='Dewar Two', dewar_type_id=3,
dewar_serial_number_id=1, tracking_number='TRACK124', dewar_serial_number_id=1, tracking_number='TRACK124',
return_address_id=2, contact_person_id=2, status='In Preparation', 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( Dewar(
id=3, dewar_name='Dewar Three', dewar_type_id=2, id=3, dewar_name='Dewar Three', dewar_type_id=2,
dewar_serial_number_id=3, tracking_number='TRACK125', dewar_serial_number_id=3, tracking_number='TRACK125',
return_address_id=1, contact_person_id=3, status='Not Shipped', 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, 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( Dewar(
id=4, dewar_name='Dewar Four', dewar_type_id=2, 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', return_address_id=1, contact_person_id=3, status='Delayed',
ready_date=datetime.strptime('2024-01-01', '%Y-%m-%d'), ready_date=datetime.strptime('2024-01-01', '%Y-%m-%d'),
shipping_date=datetime.strptime('2024-01-02', '%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( Dewar(
id=5, dewar_name='Dewar Five', dewar_type_id=1, 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', return_address_id=1, contact_person_id=3, status='Returned',
arrival_date=datetime.strptime('2024-01-03', '%Y-%m-%d'), arrival_date=datetime.strptime('2024-01-03', '%Y-%m-%d'),
returning_date=datetime.strptime('2024-01-07', '%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.orm import Session
from sqlalchemy import create_engine from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
from app import models
SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db" # Use appropriate path or database URL 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() Base = declarative_base()
# Dependency # Dependency
def get_db(): def get_db():
db = SessionLocal() db = SessionLocal()
@ -19,22 +20,18 @@ def get_db():
finally: finally:
db.close() db.close()
def init_db(): def init_db():
# Import models inside function to avoid circular dependency # Import models inside function to avoid circular dependency
from app import models from app import models
Base.metadata.create_all(bind=engine) Base.metadata.create_all(bind=engine)
def load_sample_data(session: Session): def load_sample_data(session: Session):
# Import models inside function to avoid circular dependency # 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.data import contacts, return_addresses, dewars, proposals, shipments, pucks, samples, dewar_types, serial_numbers, slots
from app import models
# If any data already exists, skip seeding # If any data already exists, skip seeding
if session.query(models.ContactPerson).first(): if session.query(models.ContactPerson).first():
return return
session.add_all(contacts + return_addresses + dewars + proposals + shipments + pucks + samples + dewar_types + serial_numbers) session.add_all(contacts + return_addresses + dewars + proposals + shipments + pucks + samples + dewar_types + serial_numbers + slots)
session.commit() session.commit()

View File

@ -73,13 +73,13 @@ class Dewar(Base):
shipping_date = Column(Date, nullable=True) shipping_date = Column(Date, nullable=True)
arrival_date = Column(Date, nullable=True) arrival_date = Column(Date, nullable=True)
returning_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) unique_id = Column(String, unique=True, index=True, nullable=True)
qrcode = Column(String, nullable=True)
shipment_id = Column(Integer, ForeignKey("shipments.id")) shipment_id = Column(Integer, ForeignKey("shipments.id"))
return_address_id = Column(Integer, ForeignKey("addresses.id")) return_address_id = Column(Integer, ForeignKey("addresses.id"))
contact_person_id = Column(Integer, ForeignKey("contact_persons.id")) contact_person_id = Column(Integer, ForeignKey("contact_persons.id"))
shipment = relationship("Shipment", back_populates="dewars") shipment = relationship("Shipment", back_populates="dewars")
events = relationship("LogisticsEvent", back_populates="dewar")
return_address = relationship("Address") return_address = relationship("Address")
contact_person = relationship("ContactPerson") contact_person = relationship("ContactPerson")
pucks = relationship("Puck", back_populates="dewar") pucks = relationship("Puck", back_populates="dewar")
@ -132,22 +132,30 @@ class Sample(Base):
puck_id = Column(Integer, ForeignKey('pucks.id')) puck_id = Column(Integer, ForeignKey('pucks.id'))
puck = relationship("Puck", back_populates="samples") puck = relationship("Puck", back_populates="samples")
class Slot(Base): class Slot(Base):
__tablename__ = "slots" __tablename__ = "slots"
id = Column(String, primary_key=True, index=True) 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) occupied = Column(Boolean, default=False)
needs_refill = Column(Boolean, default=False) needs_refill = Column(Boolean, default=False)
time_until_refill = Column(Interval, nullable=True)
last_refill = Column(DateTime, default=datetime.utcnow) 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): class LogisticsEvent(Base):
__tablename__ = "logistics_events" __tablename__ = 'logistics_events'
id = Column(Integer, primary_key=True, index=True, autoincrement=True) id = Column(Integer, primary_key=True, index=True, autoincrement=True)
dewar_id = Column(Integer, ForeignKey('dewars.id'), nullable=False) dewar_id = Column(Integer, ForeignKey('dewars.id'), nullable=False)
slot_id = Column(String, ForeignKey('slots.id'), nullable=True)
event_type = Column(String, nullable=False) event_type = Column(String, nullable=False)
timestamp = Column(DateTime, default=datetime.utcnow) timestamp = Column(DateTime, default=datetime.utcnow)
dewar = relationship("Dewar") dewar = relationship("Dewar", back_populates="events")
slot = relationship("Slot")

View File

@ -1,6 +1,4 @@
import os import os, tempfile, time, random, hashlib
import tempfile # <-- Add this import
import xml.etree.ElementTree as ET
from fastapi import APIRouter, HTTPException, status, Depends, Response from fastapi import APIRouter, HTTPException, status, Depends, Response
from sqlalchemy.orm import Session, joinedload from sqlalchemy.orm import Session, joinedload
from typing import List from typing import List
@ -38,9 +36,12 @@ from app.crud import get_shipments, get_shipment_by_id # Import CRUD functions
router = APIRouter() router = APIRouter()
def generate_unique_id(db: Session) -> str: def generate_unique_id(db: Session, length: int = 16) -> str:
while True: 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() existing_dewar = db.query(DewarModel).filter(DewarModel.unique_id == unique_id).first()
if not existing_dewar: if not existing_dewar:
break break

View File

@ -1,107 +1,113 @@
from fastapi import APIRouter, HTTPException, Depends from fastapi import APIRouter, HTTPException, Depends
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
import logging
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import List from typing import List
from app.models import Dewar as DewarModel, Slot as SlotModel, LogisticsEvent as LogisticsEventModel 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.database import get_db
from app.data import slots_data
import logging
router = APIRouter() router = APIRouter()
# Configure logging
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@router.get("/dewars", response_model=List[DewarSchema]) @router.get("/dewars", response_model=List[DewarSchema])
async def get_all_prouts(db: Session = Depends(get_db)): async def get_all_dewars(db: Session = Depends(get_db)):
try: dewars = db.query(DewarModel).all()
dewars = db.query(DewarModel).all() return dewars
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")
@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() @router.get("/dewar/{unique_id}", response_model=DewarSchema)
logger.info(f"Trimmed qr_code after stripping: {trimmed_qr_code}") 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: @router.post("/dewar/scan", response_model=dict)
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)
async def scan_dewar(event_data: LogisticsEventCreate, db: Session = Depends(get_db)): async def scan_dewar(event_data: LogisticsEventCreate, db: Session = Depends(get_db)):
dewar_qr_code = event_data.dewar_qr_code dewar_qr_code = event_data.dewar_qr_code
location_qr_code = event_data.location_qr_code location_qr_code = event_data.location_qr_code
transaction_type = event_data.transaction_type transaction_type = event_data.transaction_type
try: dewar = db.query(DewarModel).filter(DewarModel.unique_id == dewar_qr_code).first()
dewar = db.query(DewarModel).filter(DewarModel.unique_id == dewar_qr_code).first() if not dewar:
if not dewar: raise HTTPException(status_code=404, detail="Dewar not found")
raise HTTPException(status_code=404, detail="Dewar not found")
if transaction_type == 'incoming': slot = db.query(SlotModel).filter(SlotModel.qr_code == location_qr_code).first()
slot = db.query(SlotModel).filter(SlotModel.id == location_qr_code).first() if transaction_type == 'incoming':
if not slot or slot.occupied: if not slot or slot.occupied:
raise HTTPException(status_code=404, detail="Slot not found or already occupied") raise HTTPException(status_code=400, detail="Slot not found or already occupied")
slot.occupied = True slot.occupied = True
log_event(db, dewar.id, slot.id, 'incoming')
elif transaction_type == 'beamline': elif transaction_type == 'outgoing':
log_event(db, dewar.id, None, 'beamline') 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': # Log the event
slot = db.query(SlotModel).filter(SlotModel.id == location_qr_code).first() log_event(db, dewar.id, slot.id if slot else None, transaction_type)
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')
elif transaction_type == 'release': db.commit()
slot = db.query(SlotModel).filter(SlotModel.id == location_qr_code).first() return {"message": "Status updated successfully"}
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()
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): def log_event(db: Session, dewar_id: int, slot_id: str, event_type: str):
new_event = LogisticsEventModel(dewar_id=dewar_id if dewar_id else None, slot_id=slot_id, event_type=event_type) new_event = LogisticsEventModel(dewar_id=dewar_id, slot_id=slot_id, event_type=event_type)
db.add(new_event) db.add(new_event)
db.commit() 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)): 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 = [] result = []
current_time = datetime.utcnow() current_time = datetime.utcnow()
for slot in slots_needing_refill: for dewar in dewars:
time_until_next_refill = slot.last_refill + timedelta(hours=24) - current_time last_refill_event = (
result.append({ db.query(LogisticsEventModel)
'slot_id': slot.id, .filter(
'needs_refill': slot.needs_refill, LogisticsEventModel.dewar_id == dewar.id,
'time_until_refill': str(time_until_next_refill) 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 return result

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Box, Typography } from '@mui/material'; import { Box, Typography } from '@mui/material';
import styled from 'styled-components'; import styled from 'styled-components';
import Slot, { SlotData } from './Slots'; import Slot, { SlotData } from '../components/Slots';
const StorageContainer = styled(Box)` const StorageContainer = styled(Box)`
display: flex; display: flex;
@ -26,91 +26,41 @@ const StorageWrapper = styled.div`
interface StorageProps { interface StorageProps {
name: string; name: string;
selectedSlot: string | null; selectedSlot: string | null;
slotsData: SlotData[];
onSelectSlot: (slot: SlotData) => void;
} }
const Storage: React.FC<StorageProps> = ({ name, selectedSlot, slotsData, onSelectSlot }) => {
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 [highlightedSlot, setHighlightedSlot] = useState<SlotData | null>(null); const [highlightedSlot, setHighlightedSlot] = useState<SlotData | null>(null);
const handleSlotSelect = (slot: SlotData) => { const handleSlotSelect = (slot: SlotData) => {
setHighlightedSlot(slot); setHighlightedSlot(slot);
onSelectSlot(slot);
console.log('Selected slot:', slot);
}; };
console.log("Rendering Storage Component with name:", name);
return ( return (
<StorageContainer> <StorageContainer>
<Typography variant="h5">{name} Slots</Typography> <Typography variant="h5">{name} Slots</Typography>
<StorageWrapper> <StorageWrapper>
{storageSlotsData[name].map((slot) => ( {slotsData.map((slot: SlotData) => (
<Slot <Slot
key={slot.id} key={slot.id}
data={slot} data={slot}
onSelect={handleSlotSelect} onSelect={handleSlotSelect}
isSelected={selectedSlot === slot.id} isSelected={selectedSlot === slot.qr_code}
/> />
))} ))}
</StorageWrapper> </StorageWrapper>
{highlightedSlot && ( {highlightedSlot && (
<Typography variant="subtitle1"> <Typography variant="subtitle1">
Selected Slot: {highlightedSlot.id} Selected Slot: {highlightedSlot.label}
</Typography> </Typography>
)} )}
</StorageContainer> </StorageContainer>
); );
} };
export default Storage; export default Storage;