From a931bfb8ec52c39e63df3d150667ac3f72eac8e0 Mon Sep 17 00:00:00 2001 From: GotthardG <51994228+GotthardG@users.noreply.github.com> Date: Tue, 19 Nov 2024 09:56:05 +0100 Subject: [PATCH] https and ssl integration on the backend, frontend and started integration of logistics app as a separate frontend --- backend/__init__.py | 0 backend/app/data/__init__.py | 1 + backend/app/data/data.py | 25 ++-- backend/app/data/slots_data.py | 36 +++++ backend/app/database.py | 13 +- backend/app/models.py | 22 ++- backend/app/routers/dewar.py | 11 +- backend/app/routers/logistics.py | 146 ++++++++++---------- backend/app/schemas.py | 16 +-- frontend/fetch-openapi.js | 16 ++- frontend/src/components/DewarDetails.tsx | 57 ++++---- frontend/src/components/ShipmentDetails.tsx | 13 +- logistics/src/components/Slots.tsx | 101 ++++++-------- logistics/src/components/Storage.tsx | 74 ++-------- 14 files changed, 264 insertions(+), 267 deletions(-) delete mode 100644 backend/__init__.py create mode 100644 backend/app/data/slots_data.py diff --git a/backend/__init__.py b/backend/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/backend/app/data/__init__.py b/backend/app/data/__init__.py index 8d85d5b..ae8b1e9 100644 --- a/backend/app/data/__init__.py +++ b/backend/app/data/__init__.py @@ -1 +1,2 @@ from .data import contacts, return_addresses, dewars, proposals, shipments, pucks, samples, dewar_types, serial_numbers +from .slots_data import slots diff --git a/backend/app/data/data.py b/backend/app/data/data.py index 92940c2..a60c214 100644 --- a/backend/app/data/data.py +++ b/backend/app/data/data.py @@ -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 ), ] diff --git a/backend/app/data/slots_data.py b/backend/app/data/slots_data.py new file mode 100644 index 0000000..c2c2f9e --- /dev/null +++ b/backend/app/data/slots_data.py @@ -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) +] \ No newline at end of file diff --git a/backend/app/database.py b/backend/app/database.py index c166339..f1e308c 100644 --- a/backend/app/database.py +++ b/backend/app/database.py @@ -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() \ No newline at end of file diff --git a/backend/app/models.py b/backend/app/models.py index 117e0dc..87bf5f6 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -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") \ No newline at end of file + dewar = relationship("Dewar", back_populates="events") \ No newline at end of file diff --git a/backend/app/routers/dewar.py b/backend/app/routers/dewar.py index b7aedf6..372840d 100644 --- a/backend/app/routers/dewar.py +++ b/backend/app/routers/dewar.py @@ -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 diff --git a/backend/app/routers/logistics.py b/backend/app/routers/logistics.py index 6680488..63f224e 100644 --- a/backend/app/routers/logistics.py +++ b/backend/app/routers/logistics.py @@ -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 \ No newline at end of file diff --git a/backend/app/schemas.py b/backend/app/schemas.py index 49691a9..2f29d41 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -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: diff --git a/frontend/fetch-openapi.js b/frontend/fetch-openapi.js index 8d98270..87732ae 100644 --- a/frontend/fetch-openapi.js +++ b/frontend/fetch-openapi.js @@ -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}`); \ No newline at end of file diff --git a/frontend/src/components/DewarDetails.tsx b/frontend/src/components/DewarDetails.tsx index 0330963..69f881d 100644 --- a/frontend/src/components/DewarDetails.tsx +++ b/frontend/src/components/DewarDetails.tsx @@ -99,7 +99,7 @@ const DewarDetails: React.FC = ({ const [knownSerialNumbers, setKnownSerialNumbers] = useState([]); const [selectedSerialNumber, setSelectedSerialNumber] = useState(''); const [isQRCodeGenerated, setIsQRCodeGenerated] = useState(false); - const [qrCodeValue, setQrCodeValue] = useState(dewar.qrcode || ''); + const [qrCodeValue, setQrCodeValue] = useState(dewar.unique_id || ''); const qrCodeRef = useRef(null); // useEffect(() => { @@ -368,7 +368,6 @@ const DewarDetails: React.FC = ({ 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 = ({ 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 = ({ )} - - - {qrCodeValue ? ( - - - - - - - - - Label is ready for download - + + {qrCodeValue ? ( + + + + + + + + + Label is ready for download - ) : ( - No QR code available - )} - - + + ) : ( + No QR code available + )} + diff --git a/frontend/src/components/ShipmentDetails.tsx b/frontend/src/components/ShipmentDetails.tsx index 3e5e802..57cd41d 100644 --- a/frontend/src/components/ShipmentDetails.tsx +++ b/frontend/src/components/ShipmentDetails.tsx @@ -44,7 +44,6 @@ const ShipmentDetails: React.FC = ({ 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 = ({ }} > - {dewar.qrcode ? ( - + {dewar.unique_id ? ( + ) : ( = ({ 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} /> diff --git a/logistics/src/components/Slots.tsx b/logistics/src/components/Slots.tsx index 359690c..f702403 100644 --- a/logistics/src/components/Slots.tsx +++ b/logistics/src/components/Slots.tsx @@ -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 = ({ data, onSelect, isSelected }) => { - const { id, occupied, needsRefill, timeUntilRefill } = data; - +const Slot: React.FC = ({ data, isSelected, onSelect }) => { return ( - onSelect(data)} isSelected={isSelected}> - {id} - - {occupied && ( - <> - {needsRefill && } - - + onSelect(data)} + > + {data.label} + {data.dewar_name && ( + {`Dewar: ${data.dewar_name}`} )} - + {data.dewar_unique_id && ( + {`ID: ${data.dewar_unique_id}`} + )} + {data.needs_refill && } + ); -} +}; export default Slot; \ No newline at end of file diff --git a/logistics/src/components/Storage.tsx b/logistics/src/components/Storage.tsx index cb8d90e..0cdb96f 100644 --- a/logistics/src/components/Storage.tsx +++ b/logistics/src/components/Storage.tsx @@ -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 = ({ name, selectedSlot }) => { +const Storage: React.FC = ({ name, selectedSlot, slotsData, onSelectSlot }) => { const [highlightedSlot, setHighlightedSlot] = useState(null); const handleSlotSelect = (slot: SlotData) => { setHighlightedSlot(slot); + onSelectSlot(slot); + console.log('Selected slot:', slot); }; + console.log("Rendering Storage Component with name:", name); + return ( {name} Slots - {storageSlotsData[name].map((slot) => ( + {slotsData.map((slot: SlotData) => ( ))} {highlightedSlot && ( - Selected Slot: {highlightedSlot.id} + Selected Slot: {highlightedSlot.label} )} ); -} +}; export default Storage; \ No newline at end of file