from fastapi import APIRouter, HTTPException, Depends from pydantic import ValidationError from sqlalchemy.orm import Session, joinedload from typing import List, Optional from ..models import Dewar as DewarModel, Slot as SlotModel, LogisticsEvent as LogisticsEventModel from ..schemas import LogisticsEventCreate, SlotSchema, Dewar as DewarSchema from ..database import get_db import logging from datetime import datetime, timedelta router = APIRouter() logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) def calculate_time_until_refill(last_refill: Optional[datetime], refill_interval_hours: int = 1) -> int: refill_interval = timedelta(hours=refill_interval_hours) now = datetime.now() if not last_refill: return -1 # Sentinel value indicating no refill event recorded time_until_next_refill = last_refill + refill_interval - now return max(0, int(time_until_next_refill.total_seconds())) @router.post("/dewars/return", response_model=DewarSchema) async def return_to_storage(data: LogisticsEventCreate, db: Session = Depends(get_db)): logger.info(f"Returning dewar to storage: {data.dewar_qr_code} at location {data.location_qr_code}") try: # Log the incoming payload logger.info("Received payload: %s", data.json()) dewar = db.query(DewarModel).filter(DewarModel.unique_id == data.dewar_qr_code).first() if not dewar: logger.error(f"Dewar not found for unique ID: {data.dewar_qr_code}") raise HTTPException(status_code=404, detail="Dewar not found") original_slot = db.query(SlotModel).filter(SlotModel.dewar_unique_id == data.dewar_qr_code).first() if original_slot and original_slot.qr_code != data.location_qr_code: logger.error(f"Dewar {data.dewar_qr_code} is associated with slot {original_slot.qr_code}") raise HTTPException(status_code=400, detail=f"Dewar {data.dewar_qr_code} is associated with a different slot {original_slot.qr_code}.") slot = db.query(SlotModel).filter(SlotModel.qr_code == data.location_qr_code).first() if not slot: logger.error(f"Slot not found for QR code: {data.location_qr_code}") raise HTTPException(status_code=404, detail="Slot not found") if slot.occupied and slot.dewar_unique_id != data.dewar_qr_code: logger.error(f"Slot {data.location_qr_code} is already occupied by another dewar") raise HTTPException(status_code=400, detail="Selected slot is already occupied by another dewar") # Update slot with dewar information slot.dewar_unique_id = dewar.unique_id slot.occupied = True dewar.last_retrieved_timestamp = None # Log the event log_event(db, dewar.id, slot.id, "returned") db.commit() logger.info(f"Dewar {data.dewar_qr_code} successfully returned to storage slot {slot.qr_code}.") db.refresh(dewar) return dewar except ValidationError as e: logger.error(f"Validation error: {e.json()}") raise HTTPException(status_code=400, detail="Invalid payload") except Exception as e: logger.error(f"Unexpected error: {str(e)}") raise HTTPException(status_code=500, detail="Internal server error") @router.post("/dewar/scan", response_model=dict) async def scan_dewar(event_data: LogisticsEventCreate, db: Session = Depends(get_db)): logger.info(f"Received event data: {event_data}") dewar_qr_code = event_data.dewar_qr_code location_qr_code = event_data.location_qr_code transaction_type = event_data.transaction_type # Validate Dewar QR Code if not dewar_qr_code or not dewar_qr_code.strip(): logger.error("Dewar QR Code is null or empty") raise HTTPException(status_code=422, detail="Dewar QR Code cannot be null or empty") # Retrieve the Dewar dewar = db.query(DewarModel).filter(DewarModel.unique_id == dewar_qr_code).first() if not dewar: logger.error("Dewar not found") raise HTTPException(status_code=404, detail="Dewar not found") # Check for Outgoing QR Codes and set transaction type if location_qr_code in ["Outgoing X10-SA", "Outgoing X06-SA"]: transaction_type = 'outgoing' # Retrieve the Slot associated with the Dewar (for outgoing) slot = None if transaction_type == 'outgoing': slot = db.query(SlotModel).filter(SlotModel.dewar_unique_id == dewar.unique_id).first() if not slot: logger.error(f"No slot associated with dewar for outgoing: {dewar_qr_code}") raise HTTPException(status_code=404, detail="No slot associated with dewar for outgoing") # Incoming Logic if transaction_type == 'incoming': slot = db.query(SlotModel).filter(SlotModel.qr_code == location_qr_code).first() if not slot or slot.occupied: logger.error(f"Slot not found or already occupied: {location_qr_code}") raise HTTPException(status_code=400, detail="Slot not found or already occupied") slot.dewar_unique_id = dewar.unique_id slot.occupied = True elif transaction_type == 'outgoing': if not slot.occupied or slot.dewar_unique_id != dewar.unique_id: logger.error(f"Slot not valid for outgoing: {location_qr_code}") raise HTTPException(status_code=400, detail="Dewar not associated with the slot for outgoing") slot.dewar_unique_id = None slot.occupied = False elif transaction_type == 'beamline': slot = db.query(SlotModel).filter(SlotModel.qr_code == location_qr_code).first() if not slot: logger.error(f"Beamline location not found: {location_qr_code}") raise HTTPException(status_code=400, detail="Beamline location not found") dewar.beamline_location = location_qr_code logger.info(f"Dewar {dewar_qr_code} assigned to beamline {location_qr_code}") # Log the event log_event(db, dewar.id, slot.id if slot else None, transaction_type) db.commit() logger.info( f"Transaction completed: {transaction_type} for dewar {dewar_qr_code} in slot {slot.qr_code if slot else 'N/A'}") return {"message": "Status updated successfully"} @router.get("/slots", response_model=List[SlotSchema]) async def get_all_slots(db: Session = Depends(get_db)): slots = db.query(SlotModel).options(joinedload(SlotModel.dewar)).all() slots_with_refill_time = [] for slot in slots: time_until_refill = None retrievedTimestamp = None beamlineLocation = None at_beamline = False retrieved = False if slot.dewar_unique_id: # Calculate time until refill last_refill_event = db.query(LogisticsEventModel) \ .join(DewarModel, DewarModel.id == LogisticsEventModel.dewar_id) \ .filter( DewarModel.unique_id == slot.dewar.unique_id, LogisticsEventModel.event_type == "refill" ) \ .order_by(LogisticsEventModel.timestamp.desc()) \ .first() if last_refill_event: last_refill = last_refill_event.timestamp time_until_refill = calculate_time_until_refill(last_refill) else: time_until_refill = -1 # Get last retrieved timestamp last_retrieved_event = db.query(LogisticsEventModel) \ .join(DewarModel, DewarModel.id == LogisticsEventModel.dewar_id) \ .filter( DewarModel.unique_id == slot.dewar.unique_id, LogisticsEventModel.event_type == "retrieved" ) \ .order_by(LogisticsEventModel.timestamp.desc()) \ .first() if last_retrieved_event: retrievedTimestamp = last_retrieved_event.timestamp.isoformat() retrieved = True # Determine the last event excluding refills last_event = db.query(LogisticsEventModel) \ .join(DewarModel, DewarModel.id == LogisticsEventModel.dewar_id) \ .filter( DewarModel.unique_id == slot.dewar.unique_id, LogisticsEventModel.event_type != "refill" ) \ .order_by(LogisticsEventModel.timestamp.desc()) \ .first() if last_event: associated_slot = db.query(SlotModel).filter(SlotModel.id == last_event.slot_id).first() beamlineLocation = associated_slot.label if associated_slot else None at_beamline = last_event.event_type == "beamline" # Corrected the contact_person assignment contact_person = None if slot.dewar and slot.dewar.contact_person: first_name = slot.dewar.contact_person.firstname last_name = slot.dewar.contact_person.lastname contact_person = f"{first_name} {last_name}" slot_data = SlotSchema( id=slot.id, qr_code=slot.qr_code, label=slot.label, qr_base=slot.qr_base, occupied=slot.occupied, needs_refill=slot.needs_refill, dewar_unique_id=slot.dewar_unique_id, dewar_name=slot.dewar.dewar_name if slot.dewar else None, time_until_refill=time_until_refill, at_beamline=at_beamline, retrieved=retrieved, retrievedTimestamp=retrievedTimestamp, beamlineLocation=beamlineLocation, shipment_name=slot.dewar.shipment.shipment_name if slot.dewar and slot.dewar.shipment else None, contact_person=contact_person, local_contact='local contact placeholder' ) logger.info(f"Dewar retrieved: {retrieved}") logger.info(f"Dewar at: {beamlineLocation}") slots_with_refill_time.append(slot_data) return slots_with_refill_time @router.post("/dewar/refill", response_model=dict) async def refill_dewar(qr_code: str, db: Session = Depends(get_db)): logger.info(f"Refilling dewar with QR code: {qr_code}") dewar = db.query(DewarModel).filter(DewarModel.unique_id == qr_code.strip()).first() if not dewar: logger.error("Dewar not found") raise HTTPException(status_code=404, detail="Dewar not found") now = datetime.now() new_event = LogisticsEventModel( dewar_id=dewar.id, slot_id=None, event_type="refill", timestamp=now, ) db.add(new_event) db.commit() time_until_refill_seconds = calculate_time_until_refill(now) logger.info(f"Dewar refilled successfully with time_until_refill: {time_until_refill_seconds}") return {"message": "Dewar refilled successfully", "time_until_refill": time_until_refill_seconds} @router.get("/dewars", response_model=List[DewarSchema]) async def get_all_dewars(db: Session = Depends(get_db)): dewars = db.query(DewarModel).all() return dewars @router.get("/dewar/{unique_id}", response_model=DewarSchema) async def get_dewar_by_unique_id(unique_id: str, db: Session = Depends(get_db)): logger.info(f"Received request for dewar with unique_id: {unique_id}") dewar = db.query(DewarModel).filter(DewarModel.unique_id == unique_id.strip()).first() if not dewar: logger.warning(f"Dewar with unique_id '{unique_id}' not found.") raise HTTPException(status_code=404, detail="Dewar not found") logger.info(f"Returning dewar: {dewar}") return dewar def log_event(db: Session, dewar_id: int, slot_id: Optional[int], event_type: str): new_event = LogisticsEventModel( dewar_id=dewar_id, slot_id=slot_id, event_type=event_type, timestamp=datetime.now() ) db.add(new_event) db.commit() logger.info(f"Logged event: {event_type} for dewar: {dewar_id} in slot: {slot_id if slot_id else 'N/A'}")