diff --git a/backend/app/data/slots_data.py b/backend/app/data/slots_data.py index 3dc335a..a4ecc52 100644 --- a/backend/app/data/slots_data.py +++ b/backend/app/data/slots_data.py @@ -11,8 +11,8 @@ slotQRCodes = [ "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" + "X10SA-Beamline", "X06SA-Beamline", "X06DA-Beamline", + "Outgoing X10SA", "Outgoing X06SA" ] def timedelta_to_str(td: timedelta) -> str: diff --git a/backend/app/models.py b/backend/app/models.py index 4107db3..5887c13 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -143,8 +143,7 @@ class Slot(Base): qr_base = Column(String, nullable=True) occupied = Column(Boolean, default=False) needs_refill = Column(Boolean, default=False) - dewar_name = Column(String) # Ensure this field exists - dewar_unique_id = Column(String, ForeignKey('dewars.unique_id'), nullable=True) # Added field + dewar_unique_id = Column(String, ForeignKey('dewars.unique_id'), nullable=True) dewar = relationship("Dewar", back_populates="slot") events = relationship("LogisticsEvent", back_populates="slot") @@ -152,8 +151,8 @@ class LogisticsEvent(Base): __tablename__ = "logistics_events" id = Column(Integer, primary_key=True, index=True) - dewar_id = Column(Integer, ForeignKey('dewars.id')) # corrected table name - slot_id = Column(Integer, ForeignKey('slots.id')) # corrected table name + dewar_id = Column(Integer, ForeignKey('dewars.id')) + slot_id = Column(Integer, ForeignKey('slots.id')) event_type = Column(String, index=True) timestamp = Column(DateTime, default=datetime.utcnow) dewar = relationship("Dewar", back_populates="events") diff --git a/backend/app/routers/logistics.py b/backend/app/routers/logistics.py index c98e792..46d7d15 100644 --- a/backend/app/routers/logistics.py +++ b/backend/app/routers/logistics.py @@ -1,5 +1,5 @@ from fastapi import APIRouter, HTTPException, Depends -from sqlalchemy.orm import Session +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 @@ -13,7 +13,7 @@ logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) -def calculate_time_until_refill(last_refill: Optional[datetime], refill_interval_hours: int = 24) -> int: +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() @@ -24,42 +24,124 @@ def calculate_time_until_refill(last_refill: Optional[datetime], refill_interval return max(0, int(time_until_next_refill.total_seconds())) +@router.post("/dewars/retrieve", response_model=DewarSchema) +async def retrieve_dewar(data: LogisticsEventCreate, db: Session = Depends(get_db)): + logger.info(f"Received data for retrieve_dewar: {data}") + dewar = db.query(DewarModel).filter(DewarModel.unique_id == data.dewar_qr_code).first() + if not dewar: + raise HTTPException(status_code=404, detail="Dewar not found") + + new_event = LogisticsEventModel( + dewar_id=dewar.id, + slot_id=None, + event_type="retrieved", + timestamp=datetime.now(), + ) + db.add(new_event) + db.commit() + db.refresh(dewar) + + # Get the last retrieved event for the dewar + last_retrieved_event = db.query(LogisticsEventModel).filter( + LogisticsEventModel.dewar_id == dewar.id, + LogisticsEventModel.event_type == "retrieved" + ).order_by(LogisticsEventModel.timestamp.desc()).first() + + if last_retrieved_event: + dewar.last_retrieved_timestamp = last_retrieved_event.timestamp.isoformat() + logger.info(f"Last retrieved event timestamp for dewar {dewar.unique_id}: " + f"{dewar.last_retrieved_timestamp}") + else: + dewar.last_retrieved_timestamp = None + logger.info(f"No retrieved event found for dewar {dewar.unique_id}") + + return dewar + + +@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 + + dewar = db.query(DewarModel).filter(DewarModel.unique_id == dewar_qr_code).first() + if not dewar: + raise HTTPException(status_code=404, detail="Dewar not found") + + 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.dewar_unique_id = dewar.unique_id + slot.occupied = True + elif transaction_type == 'outgoing': + if not slot or not slot.occupied or slot.dewar_unique_id != dewar.unique_id: + raise HTTPException(status_code=400, detail="Slot not found or dewar not associated with slot") + slot.dewar_unique_id = None + slot.occupied = False + elif transaction_type == 'beamline': + if not slot: + raise HTTPException(status_code=400, detail="Beamline location not found") + dewar.beamline_location = location_qr_code + + # Create a logistics event + log_event(db, dewar.id, slot.id if slot else None, transaction_type) + + db.commit() + 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).all() + slots = db.query(SlotModel).options(joinedload(SlotModel.dewar)).all() slots_with_refill_time = [] for slot in slots: time_until_refill = None - if slot.dewar_unique_id: - logger.info(f"Fetching last refill event for dewar: {slot.dewar_unique_id}") + retrievedTimestamp = None - try: - 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() - except Exception as e: - logger.error(f"Error querying last refill event: {str(e)}") - last_refill_event = None + if slot.dewar_unique_id: + 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) - logger.info(f"Slot ID: {slot.id}, Last Refill: {last_refill}, Time Until Refill: {time_until_refill}") else: - logger.warning(f"Slot ID: {slot.id} for dewar id '{slot.dewar_unique_id}' has no refill events.") time_until_refill = -1 - else: - logger.warning(f"Slot ID: {slot.id} has no dewar associated.") - time_until_refill = -1 - # Ensure Dewar.name is optional if it may not exist in Dewar schema. - dewar_name = slot.dewar.name if slot.dewar and hasattr(slot.dewar, 'name') else None + # 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() + + # Determine if the dewar is at the beamline + at_beamline = False + if slot.dewar_unique_id: + at_beamline_event = db.query(LogisticsEventModel) \ + .join(DewarModel, DewarModel.id == LogisticsEventModel.dewar_id) \ + .filter( + DewarModel.unique_id == slot.dewar.unique_id, + LogisticsEventModel.event_type == "beamline" + ) \ + .order_by(LogisticsEventModel.timestamp.desc()) \ + .first() + at_beamline = bool(at_beamline_event) slot_data = SlotSchema( id=slot.id, @@ -69,29 +151,23 @@ async def get_all_slots(db: Session = Depends(get_db)): occupied=slot.occupied, needs_refill=slot.needs_refill, dewar_unique_id=slot.dewar_unique_id, - dewar_name=dewar_name, # Using the optional dewar_name - time_until_refill=(time_until_refill or -1) + dewar_name=slot.dewar.dewar_name if slot.dewar else None, + time_until_refill=time_until_refill, + at_beamline=at_beamline, + retrievedTimestamp=retrievedTimestamp, ) - + logger.info(f"Dewar retrieved on the: {retrievedTimestamp}") slots_with_refill_time.append(slot_data) - return slots_with_refill_time -@router.get("/dewars", response_model=List[DewarSchema]) -async def get_all_dewars(db: Session = Depends(get_db)): - dewars = db.query(DewarModel).all() - return dewars + 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}") - if not isinstance(qr_code, str): - raise HTTPException(status_code=400, detail="Invalid QR code format") - 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") @@ -112,6 +188,13 @@ async def refill_dewar(qr_code: str, db: Session = Depends(get_db)): 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}") @@ -123,39 +206,13 @@ async def get_dewar_by_unique_id(unique_id: str, db: Session = Depends(get_db)): return dewar -@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 - - logger.info(f"Scanning dewar {dewar_qr_code} for slot {location_qr_code} with transaction type {transaction_type}") - - dewar = db.query(DewarModel).filter(DewarModel.unique_id == dewar_qr_code).first() - if not dewar: - raise HTTPException(status_code=404, detail="Dewar not found") - - 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") - logger.info(f"Associating dewar {dewar.unique_id} with slot {slot.qr_code}") - slot.dewar_unique_id = dewar.unique_id # Properly associate with the unique_id - slot.occupied = True - elif transaction_type == 'outgoing': - if not slot or not slot.occupied or slot.dewar_unique_id != dewar.unique_id: - raise HTTPException(status_code=400, detail="Slot not found or dewar not associated with slot") - logger.info(f"Disassociating dewar {dewar.unique_id} from slot {slot.qr_code}") - slot.dewar_unique_id = None # Remove the association - slot.occupied = False - - log_event(db, dewar.id, slot.id if slot else None, transaction_type) - db.commit() - return {"message": "Status updated successfully"} - - -def log_event(db: Session, dewar_id: int, slot_id: int, event_type: str): - new_event = LogisticsEventModel(dewar_id=dewar_id, slot_id=slot_id, event_type=event_type) +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() \ No newline at end of file + db.commit() + logger.info(f"Logged event: {event_type} for dewar: {dewar_id} in slot: {slot_id if slot_id else 'N/A'}") \ No newline at end of file diff --git a/backend/app/schemas.py b/backend/app/schemas.py index 69a4a41..f5d8926 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -286,7 +286,9 @@ class SlotSchema(BaseModel): needs_refill: bool dewar_unique_id: Optional[str] dewar_name: Optional[str] - time_until_refill: Optional[int] # Ensure this field is defined + time_until_refill: Optional[int] + at_beamline: Optional[bool] + retrievedTimestamp: Optional[str] class Config: from_attributes = True \ No newline at end of file diff --git a/logistics/src/assets/50565__broumbroum__sf3-sfx-menu-validate.wav b/logistics/src/assets/50565__broumbroum__sf3-sfx-menu-validate.wav new file mode 100644 index 0000000..77f0f43 Binary files /dev/null and b/logistics/src/assets/50565__broumbroum__sf3-sfx-menu-validate.wav differ diff --git a/logistics/src/components/Slots.tsx b/logistics/src/components/Slots.tsx index 96139bb..04ee73f 100644 --- a/logistics/src/components/Slots.tsx +++ b/logistics/src/components/Slots.tsx @@ -2,8 +2,8 @@ import React, { useEffect } from 'react'; import { Box, Typography, Button, Alert } from '@mui/material'; import styled from 'styled-components'; import LocalGasStationIcon from '@mui/icons-material/LocalGasStation'; +import LocationOnIcon from '@mui/icons-material/LocationOn'; // New import for location indication import CountdownTimer from './CountdownTimer'; -import QRCode from 'react-qr-code'; export interface SlotData { id: string; @@ -15,6 +15,7 @@ export interface SlotData { dewar_name?: string; needs_refill?: boolean; time_until_refill?: number; + at_beamline?: boolean; // New property to indicate the dewar is at the beamline } interface SlotProps { @@ -28,6 +29,7 @@ interface SlotProps { interface StyledSlotProps { isSelected: boolean; isOccupied: boolean; + atBeamline: boolean; } const StyledSlot = styled(Box)` @@ -35,8 +37,8 @@ const StyledSlot = styled(Box)` margin: 8px; width: 150px; height: 260px; - background-color: ${({ isSelected, isOccupied }) => - isSelected ? '#3f51b5' : isOccupied ? '#f44336' : '#4caf50'}; + background-color: ${({ isSelected, isOccupied, atBeamline }) => + atBeamline ? '#ff9800' : isSelected ? '#3f51b5' : isOccupied ? '#f44336' : '#4caf50'}; color: white; cursor: pointer; display: flex; @@ -52,12 +54,6 @@ const StyledSlot = styled(Box)` } `; -const QRCodeContainer = styled.div` - padding: 8px; - background-color: white; - border-radius: 8px; -`; - const BottleIcon: React.FC<{ fillHeight: number }> = ({ fillHeight }) => { const pixelHeight = (276.777 * fillHeight) / 100; const yPosition = 276.777 - pixelHeight; @@ -80,49 +76,38 @@ const BottleIcon: React.FC<{ fillHeight: number }> = ({ fillHeight }) => { }; const Slot: React.FC = ({ data, isSelected, onSelect, onRefillDewar, reloadSlots }) => { + const { id, qr_code, label, qr_base, occupied, needs_refill, time_until_refill, dewar_unique_id, dewar_name, at_beamline } = data; + const calculateFillHeight = (timeUntilRefill?: number) => { if (timeUntilRefill === undefined || timeUntilRefill <= 0) { return 0; } - const maxTime = 86400; + const maxTime = 3600; return Math.min((timeUntilRefill / maxTime) * 100, 100); }; - const fillHeight = calculateFillHeight(data.time_until_refill); + const fillHeight = calculateFillHeight(time_until_refill); useEffect(() => { - if (data.time_until_refill !== undefined) { - console.log(`Updated time_until_refill: ${data.time_until_refill}`); + if (time_until_refill !== undefined) { + console.log(`Updated time_until_refill: ${time_until_refill}`); } - }, [data.time_until_refill]); + }, [time_until_refill]); const handleRefill = async () => { - if (data.dewar_unique_id) { - await onRefillDewar(data.dewar_unique_id); + if (dewar_unique_id) { + await onRefillDewar(dewar_unique_id); reloadSlots(); } }; - const { id, qr_code, label, qr_base, occupied, needs_refill, time_until_refill, dewar_unique_id, dewar_name, ...rest } = data; - return ( - onSelect(data)} - {...rest} - > + onSelect(data)}> {label} - {dewar_name && {`Dewar: ${dewar_name}`}} - {dewar_unique_id && ( - - - - )} + {dewar_name && {`${dewar_name}`}} {/* Ensure correct dewar_name */} {needs_refill && } - {dewar_unique_id && ( - - )} + {dewar_unique_id && } + {at_beamline && At Beamline} {/* Indicate at beamline */} {(dewar_unique_id && time_until_refill !== undefined && time_until_refill !== -1) ? ( ) : null} @@ -136,6 +121,6 @@ const Slot: React.FC = ({ data, isSelected, onSelect, onRefillDewar, )} ); -}; +} export default Slot; \ No newline at end of file