diff --git a/backend/app/data/slots_data.py b/backend/app/data/slots_data.py index c2c2f9e..3dc335a 100644 --- a/backend/app/data/slots_data.py +++ b/backend/app/data/slots_data.py @@ -29,8 +29,6 @@ slots = [ 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/models.py b/backend/app/models.py index 21ca17e..469448c 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -143,19 +143,11 @@ class Slot(Base): qr_base = Column(String, nullable=True) occupied = Column(Boolean, default=False) needs_refill = Column(Boolean, default=False) - last_refill = Column(DateTime, default=datetime.utcnow) - time_until_refill = Column(Integer) # store as total seconds + dewar_name = Column(String) # Ensure this field exists dewar_unique_id = Column(String, ForeignKey('dewars.unique_id'), nullable=True) # Added field dewar = relationship("Dewar", back_populates="slot") events = relationship("LogisticsEvent", back_populates="slot") - - @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" diff --git a/backend/app/routers/logistics.py b/backend/app/routers/logistics.py index efc2779..f38f5d2 100644 --- a/backend/app/routers/logistics.py +++ b/backend/app/routers/logistics.py @@ -1,9 +1,9 @@ from fastapi import APIRouter, HTTPException, Depends from sqlalchemy.orm import Session -from typing import List -from app.models import Dewar as DewarModel, Slot as SlotModel, LogisticsEvent as LogisticsEventModel -from app.schemas import LogisticsEventCreate, Slot as SlotSchema, Dewar as DewarSchema -from app.database import get_db +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 @@ -12,17 +12,55 @@ router = APIRouter() logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) -def calculate_time_until_refill(last_refill: datetime) -> int: - refill_interval = timedelta(hours=24) # Example interval + +def calculate_time_until_refill(last_refill: Optional[datetime], refill_interval_hours: int = 24) -> 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 int(time_until_next_refill.total_seconds()) + return max(0, int(time_until_next_refill.total_seconds())) + @router.get("/slots", response_model=List[SlotSchema]) async def get_all_slots(db: Session = Depends(get_db)): slots = db.query(SlotModel).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}") + + 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 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 + slot_data = SlotSchema( id=slot.id, qr_code=slot.qr_code, @@ -30,12 +68,13 @@ async def get_all_slots(db: Session = Depends(get_db)): qr_base=slot.qr_base, occupied=slot.occupied, needs_refill=slot.needs_refill, - last_refill=slot.last_refill, - time_until_refill=calculate_time_until_refill(slot.last_refill), dewar_unique_id=slot.dewar_unique_id, - dewar_name=slot.dewar.dewar_name if slot.dewar else None + dewar_name=dewar_name, # Using the optional dewar_name + time_until_refill=(time_until_refill or -1) ) + slots_with_refill_time.append(slot_data) + return slots_with_refill_time @router.get("/dewars", response_model=List[DewarSchema]) @@ -43,6 +82,37 @@ async def get_all_dewars(db: Session = Depends(get_db)): dewars = db.query(DewarModel).all() return dewars + +@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") + + now = datetime.now() + + new_event = LogisticsEventModel( + dewar_id=dewar.id, + slot_id=None, + event_type="refill", + timestamp=now, + action_details=f"{dewar.unique_id} refilled" + ) + 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("/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}") @@ -53,13 +123,14 @@ async def get_dewar_by_unique_id(unique_id: str, db: Session = Depends(get_db)): logger.info(f"Returning dewar: {dewar}") 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 - print(f"Scanning dewar {dewar_qr_code} for slot {location_qr_code} with transaction type {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: @@ -70,13 +141,13 @@ async def scan_dewar(event_data: LogisticsEventCreate, db: Session = Depends(get if transaction_type == 'incoming': if not slot or slot.occupied: raise HTTPException(status_code=400, detail="Slot not found or already occupied") - print(f"Associating dewar {dewar.unique_id} with slot {slot.qr_code}") + 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") - print(f"Disassociating dewar {dewar.unique_id} from slot {slot.qr_code}") + logger.info(f"Disassociating dewar {dewar.unique_id} from slot {slot.qr_code}") slot.dewar_unique_id = None # Remove the association slot.occupied = False @@ -84,31 +155,8 @@ async def scan_dewar(event_data: LogisticsEventCreate, db: Session = Depends(get 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) db.add(new_event) - db.commit() - - -@router.post("/dewar/refill", response_model=dict) -async def refill_dewar(qr_code: str, db: Session = Depends(get_db)): - dewar = db.query(DewarModel).filter(DewarModel.unique_id == qr_code).first() - if not dewar: - raise HTTPException(status_code=404, detail="Dewar not found") - - # Process refill - dewar.last_refill = datetime.now() - - # Calculate and update time until next refill - time_until_refill_seconds = calculate_time_until_refill(dewar.last_refill) - db.query(SlotModel).filter(SlotModel.dewar_unique_id == dewar.unique_id).update( - {'time_until_refill': time_until_refill_seconds}) - - new_event = LogisticsEventModel( - dewar_id=dewar.id, slot_id=None, # No specific slot, as it's a refill event - event_type="refill", - action_details=f"{dewar.unique_id} refilled" - ) - db.add(new_event) - db.commit() - return {"message": "Dewar refilled successfully"} \ No newline at end of file + db.commit() \ No newline at end of file diff --git a/backend/app/schemas.py b/backend/app/schemas.py index be63259..69a4a41 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -277,17 +277,16 @@ class LogisticsEventCreate(BaseModel): location_qr_code: str transaction_type: str -class Slot(BaseModel): +class SlotSchema(BaseModel): id: str qr_code: str label: str qr_base: Optional[str] occupied: bool needs_refill: bool - last_refill: datetime - time_until_refill: int # Can't be Optional - dewar_unique_id: Optional[str] # Ensure this field exists - dewar: Optional[Dewar] = None # Add this field + dewar_unique_id: Optional[str] + dewar_name: Optional[str] + time_until_refill: Optional[int] # Ensure this field is defined class Config: from_attributes = True \ No newline at end of file diff --git a/logistics/src/components/CountdownTimer.tsx b/logistics/src/components/CountdownTimer.tsx index a70b932..6b6b211 100644 --- a/logistics/src/components/CountdownTimer.tsx +++ b/logistics/src/components/CountdownTimer.tsx @@ -9,12 +9,14 @@ const CountdownTimer: React.FC = ({ totalSeconds }) => { const [timeLeft, setTimeLeft] = useState(totalSeconds); useEffect(() => { + setTimeLeft(totalSeconds); // Reset timer on prop change + const timerId = setInterval(() => { setTimeLeft(prev => Math.max(prev - 1, 0)); }, 1000); return () => clearInterval(timerId); - }, []); + }, [totalSeconds]); // Listen to changes in totalSeconds const formatTime = (seconds: number) => { const hrs = Math.floor(seconds / 3600); @@ -23,8 +25,10 @@ const CountdownTimer: React.FC = ({ totalSeconds }) => { return `${hrs}h ${min}m ${sec}s`; }; + const timeColor = timeLeft < 300 ? 'red' : 'white'; // Red if less than 5 minutes remaining + return ( - {/* Warn with red color if less than 5 minutes */} + {`Time until refill: ${formatTime(timeLeft)}`} ); diff --git a/logistics/src/components/Slots.tsx b/logistics/src/components/Slots.tsx index e6afec5..1bc0aa7 100644 --- a/logistics/src/components/Slots.tsx +++ b/logistics/src/components/Slots.tsx @@ -1,8 +1,8 @@ -import React from 'react'; -import { Box, Typography } from '@mui/material'; +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'; // Icon for refilling indicator. -import CountdownTimer from './CountdownTimer'; // Import the CountdownTimer component +import LocalGasStationIcon from '@mui/icons-material/LocalGasStation'; +import CountdownTimer from './CountdownTimer'; export interface SlotData { id: string; @@ -10,23 +10,30 @@ export interface SlotData { label: string; qr_base: string; occupied: boolean; - dewar_unique_id?: string; // Optional additional information. - dewar_name?: string; // Optional dewar information. - needs_refill?: boolean; // Indicator for refill requirement. - time_until_refill?: number; // Time until refill in seconds, optional field + dewar_unique_id?: string; + dewar_name?: string; + needs_refill?: boolean; + time_until_refill?: number; } interface SlotProps { data: SlotData; isSelected: boolean; - onSelect: (slot: SlotData) => void; + onSelect: (data: SlotData) => void; + onRefillDewar: (qr_code?: string) => Promise; + reloadSlots: () => Promise; } -const StyledSlot = styled(Box)<{ isSelected: boolean; isOccupied: boolean }>` +interface StyledSlotProps { + isSelected: boolean; + isOccupied: boolean; +} + +const StyledSlot = styled(Box)` padding: 16px; margin: 8px; - width: 150px; // Increase the width to accommodate more info. - height: 200px; // Increase the height to accommodate more info. + width: 150px; + height: 220px; background-color: ${({ isSelected, isOccupied }) => isSelected ? '#3f51b5' : isOccupied ? '#f44336' : '#4caf50'}; color: white; @@ -44,24 +51,41 @@ const StyledSlot = styled(Box)<{ isSelected: boolean; isOccupied: boolean }>` } `; -const Slot: React.FC = ({ data, isSelected, onSelect }) => { +const Slot: React.FC = ({ data, isSelected, onSelect, onRefillDewar, reloadSlots }) => { + useEffect(() => { + console.log(`Updated time_until_refill: ${data.time_until_refill}`); + }, [data.time_until_refill]); + + const handleRefill = async () => { + if (data.dewar_unique_id) { + await onRefillDewar(data.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} > - {data.label} - {data.dewar_name && ( - {`Dewar: ${data.dewar_name}`} + {label} + {dewar_name && {`Dewar: ${dewar_name}`}} + {dewar_unique_id && {`ID: ${dewar_unique_id}`}} + {needs_refill && } + {dewar_unique_id && time_until_refill !== undefined && time_until_refill !== -1 && ( + )} - {data.dewar_unique_id && ( - {`ID: ${data.dewar_unique_id}`} + {needs_refill && ( + )} - {data.needs_refill && } - {/* Display countdown timer only for slots that have an associated dewar */} - {data.dewar_unique_id && data.time_until_refill !== undefined && ( - + {occupied && time_until_refill === -1 && ( + This dewar has no recorded refill event. It needs to be refilled. )} ); diff --git a/logistics/src/components/Storage.tsx b/logistics/src/components/Storage.tsx index 566c67f..ab2b603 100644 --- a/logistics/src/components/Storage.tsx +++ b/logistics/src/components/Storage.tsx @@ -1,5 +1,5 @@ -import React, { useState } from 'react'; -import { Box, Typography } from '@mui/material'; +import React from 'react'; +import { Box, Typography, Button } from '@mui/material'; import styled from 'styled-components'; import Slot, { SlotData } from '../components/Slots'; @@ -28,9 +28,10 @@ interface StorageProps { selectedSlot: string | null; slotsData: SlotData[]; onSelectSlot: (slot: SlotData) => void; + onRefillDewar: (slot: SlotData) => void; // Adjusted this prop to pass SlotData object } -const Storage: React.FC = ({ name, selectedSlot, slotsData, onSelectSlot }) => { +const Storage: React.FC = ({ name, selectedSlot, slotsData, onSelectSlot, onRefillDewar }) => { const handleSlotSelect = (slot: SlotData) => { onSelectSlot(slot); console.log('Selected slot:', slot); @@ -46,9 +47,23 @@ const Storage: React.FC = ({ name, selectedSlot, slotsData, onSele data={slot} onSelect={handleSlotSelect} isSelected={selectedSlot === slot.qr_code} + onRefillDewar={onRefillDewar} // Pass the refill handler to Slot component /> ))} + ); };