now with a working countdowntimer for each dewar

This commit is contained in:
GotthardG 2024-11-20 22:37:18 +01:00
parent cacf43b631
commit db610da588
7 changed files with 162 additions and 82 deletions

View File

@ -29,8 +29,6 @@ slots = [
qr_base=qrcode.split('-')[1] if '-' in qrcode else '', qr_base=qrcode.split('-')[1] if '-' in qrcode else '',
occupied=False, occupied=False,
needs_refill=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) for i, qrcode in enumerate(slotQRCodes)
] ]

View File

@ -143,19 +143,11 @@ class Slot(Base):
qr_base = Column(String, nullable=True) 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)
last_refill = Column(DateTime, default=datetime.utcnow) dewar_name = Column(String) # Ensure this field exists
time_until_refill = Column(Integer) # store as total seconds
dewar_unique_id = Column(String, ForeignKey('dewars.unique_id'), nullable=True) # Added field dewar_unique_id = Column(String, ForeignKey('dewars.unique_id'), nullable=True) # Added field
dewar = relationship("Dewar", back_populates="slot") dewar = relationship("Dewar", back_populates="slot")
events = relationship("LogisticsEvent", 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): class LogisticsEvent(Base):
__tablename__ = "logistics_events" __tablename__ = "logistics_events"

View File

@ -1,9 +1,9 @@
from fastapi import APIRouter, HTTPException, Depends from fastapi import APIRouter, HTTPException, Depends
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from typing import List from typing import List, Optional
from app.models import Dewar as DewarModel, Slot as SlotModel, LogisticsEvent as LogisticsEventModel from ..models import Dewar as DewarModel, Slot as SlotModel, LogisticsEvent as LogisticsEventModel
from app.schemas import LogisticsEventCreate, Slot as SlotSchema, Dewar as DewarSchema from ..schemas import LogisticsEventCreate, SlotSchema, Dewar as DewarSchema
from app.database import get_db from ..database import get_db
import logging import logging
from datetime import datetime, timedelta from datetime import datetime, timedelta
@ -12,17 +12,55 @@ router = APIRouter()
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__) 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() 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 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]) @router.get("/slots", response_model=List[SlotSchema])
async def get_all_slots(db: Session = Depends(get_db)): async def get_all_slots(db: Session = Depends(get_db)):
slots = db.query(SlotModel).all() slots = db.query(SlotModel).all()
slots_with_refill_time = [] slots_with_refill_time = []
for slot in slots: 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( slot_data = SlotSchema(
id=slot.id, id=slot.id,
qr_code=slot.qr_code, qr_code=slot.qr_code,
@ -30,12 +68,13 @@ async def get_all_slots(db: Session = Depends(get_db)):
qr_base=slot.qr_base, qr_base=slot.qr_base,
occupied=slot.occupied, occupied=slot.occupied,
needs_refill=slot.needs_refill, 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_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) slots_with_refill_time.append(slot_data)
return slots_with_refill_time return slots_with_refill_time
@router.get("/dewars", response_model=List[DewarSchema]) @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() dewars = db.query(DewarModel).all()
return dewars 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) @router.get("/dewar/{unique_id}", response_model=DewarSchema)
async def get_dewar_by_unique_id(unique_id: str, db: Session = Depends(get_db)): 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}") 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}") logger.info(f"Returning dewar: {dewar}")
return dewar return dewar
@router.post("/dewar/scan", response_model=dict) @router.post("/dewar/scan", response_model=dict)
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
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() dewar = db.query(DewarModel).filter(DewarModel.unique_id == dewar_qr_code).first()
if not dewar: if not dewar:
@ -70,13 +141,13 @@ async def scan_dewar(event_data: LogisticsEventCreate, db: Session = Depends(get
if transaction_type == 'incoming': if transaction_type == 'incoming':
if not slot or slot.occupied: if not slot or slot.occupied:
raise HTTPException(status_code=400, detail="Slot not found or already 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.dewar_unique_id = dewar.unique_id # Properly associate with the unique_id
slot.occupied = True slot.occupied = True
elif transaction_type == 'outgoing': elif transaction_type == 'outgoing':
if not slot or not slot.occupied or slot.dewar_unique_id != dewar.unique_id: 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") 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.dewar_unique_id = None # Remove the association
slot.occupied = False slot.occupied = False
@ -84,31 +155,8 @@ async def scan_dewar(event_data: LogisticsEventCreate, db: Session = Depends(get
db.commit() db.commit()
return {"message": "Status updated successfully"} return {"message": "Status updated successfully"}
def log_event(db: Session, dewar_id: int, slot_id: int, event_type: str): 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) 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()
@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"}

View File

@ -277,17 +277,16 @@ class LogisticsEventCreate(BaseModel):
location_qr_code: str location_qr_code: str
transaction_type: str transaction_type: str
class Slot(BaseModel): class SlotSchema(BaseModel):
id: str id: str
qr_code: str qr_code: str
label: str label: str
qr_base: Optional[str] qr_base: Optional[str]
occupied: bool occupied: bool
needs_refill: bool needs_refill: bool
last_refill: datetime dewar_unique_id: Optional[str]
time_until_refill: int # Can't be Optional dewar_name: Optional[str]
dewar_unique_id: Optional[str] # Ensure this field exists time_until_refill: Optional[int] # Ensure this field is defined
dewar: Optional[Dewar] = None # Add this field
class Config: class Config:
from_attributes = True from_attributes = True

View File

@ -9,12 +9,14 @@ const CountdownTimer: React.FC<CountdownTimerProps> = ({ totalSeconds }) => {
const [timeLeft, setTimeLeft] = useState(totalSeconds); const [timeLeft, setTimeLeft] = useState(totalSeconds);
useEffect(() => { useEffect(() => {
setTimeLeft(totalSeconds); // Reset timer on prop change
const timerId = setInterval(() => { const timerId = setInterval(() => {
setTimeLeft(prev => Math.max(prev - 1, 0)); setTimeLeft(prev => Math.max(prev - 1, 0));
}, 1000); }, 1000);
return () => clearInterval(timerId); return () => clearInterval(timerId);
}, []); }, [totalSeconds]); // Listen to changes in totalSeconds
const formatTime = (seconds: number) => { const formatTime = (seconds: number) => {
const hrs = Math.floor(seconds / 3600); const hrs = Math.floor(seconds / 3600);
@ -23,8 +25,10 @@ const CountdownTimer: React.FC<CountdownTimerProps> = ({ totalSeconds }) => {
return `${hrs}h ${min}m ${sec}s`; return `${hrs}h ${min}m ${sec}s`;
}; };
const timeColor = timeLeft < 300 ? 'red' : 'white'; // Red if less than 5 minutes remaining
return ( return (
<Typography variant="body2" style={{ color: timeLeft < 300 ? 'red' : 'white' }} > {/* Warn with red color if less than 5 minutes */} <Typography variant="body2" style={{ color: timeColor }}>
{`Time until refill: ${formatTime(timeLeft)}`} {`Time until refill: ${formatTime(timeLeft)}`}
</Typography> </Typography>
); );

View File

@ -1,8 +1,8 @@
import React from 'react'; import React, { useEffect } from 'react';
import { Box, Typography } from '@mui/material'; import { Box, Typography, Button, Alert } from '@mui/material';
import styled from 'styled-components'; import styled from 'styled-components';
import LocalGasStationIcon from '@mui/icons-material/LocalGasStation'; // Icon for refilling indicator. import LocalGasStationIcon from '@mui/icons-material/LocalGasStation';
import CountdownTimer from './CountdownTimer'; // Import the CountdownTimer component import CountdownTimer from './CountdownTimer';
export interface SlotData { export interface SlotData {
id: string; id: string;
@ -10,23 +10,30 @@ export interface SlotData {
label: string; label: string;
qr_base: string; qr_base: string;
occupied: boolean; occupied: boolean;
dewar_unique_id?: string; // Optional additional information. dewar_unique_id?: string;
dewar_name?: string; // Optional dewar information. dewar_name?: string;
needs_refill?: boolean; // Indicator for refill requirement. needs_refill?: boolean;
time_until_refill?: number; // Time until refill in seconds, optional field time_until_refill?: number;
} }
interface SlotProps { interface SlotProps {
data: SlotData; data: SlotData;
isSelected: boolean; isSelected: boolean;
onSelect: (slot: SlotData) => void; onSelect: (data: SlotData) => void;
onRefillDewar: (qr_code?: string) => Promise<void>;
reloadSlots: () => Promise<void>;
} }
const StyledSlot = styled(Box)<{ isSelected: boolean; isOccupied: boolean }>` interface StyledSlotProps {
isSelected: boolean;
isOccupied: boolean;
}
const StyledSlot = styled(Box)<StyledSlotProps>`
padding: 16px; padding: 16px;
margin: 8px; margin: 8px;
width: 150px; // Increase the width to accommodate more info. width: 150px;
height: 200px; // Increase the height to accommodate more info. height: 220px;
background-color: ${({ isSelected, isOccupied }) => background-color: ${({ isSelected, isOccupied }) =>
isSelected ? '#3f51b5' : isOccupied ? '#f44336' : '#4caf50'}; isSelected ? '#3f51b5' : isOccupied ? '#f44336' : '#4caf50'};
color: white; color: white;
@ -44,24 +51,41 @@ const StyledSlot = styled(Box)<{ isSelected: boolean; isOccupied: boolean }>`
} }
`; `;
const Slot: React.FC<SlotProps> = ({ data, isSelected, onSelect }) => { const Slot: React.FC<SlotProps> = ({ 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 ( return (
<StyledSlot <StyledSlot
isSelected={isSelected} isSelected={isSelected}
isOccupied={data.occupied} isOccupied={occupied}
onClick={() => onSelect(data)} onClick={() => onSelect(data)}
{...rest}
> >
<Typography variant="h6">{data.label}</Typography> <Typography variant="h6">{label}</Typography>
{data.dewar_name && ( {dewar_name && <Typography variant="body2">{`Dewar: ${dewar_name}`}</Typography>}
<Typography variant="body2">{`Dewar: ${data.dewar_name}`}</Typography> {dewar_unique_id && <Typography variant="body2">{`ID: ${dewar_unique_id}`}</Typography>}
{needs_refill && <LocalGasStationIcon />}
{dewar_unique_id && time_until_refill !== undefined && time_until_refill !== -1 && (
<CountdownTimer key={dewar_unique_id} totalSeconds={time_until_refill} />
)} )}
{data.dewar_unique_id && ( {needs_refill && (
<Typography variant="body2">{`ID: ${data.dewar_unique_id}`}</Typography> <Button onClick={handleRefill} sx={{ mt: 1, color: 'white' }}>
Refill
</Button>
)} )}
{data.needs_refill && <LocalGasStationIcon />} {occupied && time_until_refill === -1 && (
{/* Display countdown timer only for slots that have an associated dewar */} <Alert severity="warning">This dewar has no recorded refill event. It needs to be refilled.</Alert>
{data.dewar_unique_id && data.time_until_refill !== undefined && (
<CountdownTimer totalSeconds={data.time_until_refill} />
)} )}
</StyledSlot> </StyledSlot>
); );

View File

@ -1,5 +1,5 @@
import React, { useState } from 'react'; import React from 'react';
import { Box, Typography } from '@mui/material'; import { Box, Typography, Button } from '@mui/material';
import styled from 'styled-components'; import styled from 'styled-components';
import Slot, { SlotData } from '../components/Slots'; import Slot, { SlotData } from '../components/Slots';
@ -28,9 +28,10 @@ interface StorageProps {
selectedSlot: string | null; selectedSlot: string | null;
slotsData: SlotData[]; slotsData: SlotData[];
onSelectSlot: (slot: SlotData) => void; onSelectSlot: (slot: SlotData) => void;
onRefillDewar: (slot: SlotData) => void; // Adjusted this prop to pass SlotData object
} }
const Storage: React.FC<StorageProps> = ({ name, selectedSlot, slotsData, onSelectSlot }) => { const Storage: React.FC<StorageProps> = ({ name, selectedSlot, slotsData, onSelectSlot, onRefillDewar }) => {
const handleSlotSelect = (slot: SlotData) => { const handleSlotSelect = (slot: SlotData) => {
onSelectSlot(slot); onSelectSlot(slot);
console.log('Selected slot:', slot); console.log('Selected slot:', slot);
@ -46,9 +47,23 @@ const Storage: React.FC<StorageProps> = ({ name, selectedSlot, slotsData, onSele
data={slot} data={slot}
onSelect={handleSlotSelect} onSelect={handleSlotSelect}
isSelected={selectedSlot === slot.qr_code} isSelected={selectedSlot === slot.qr_code}
onRefillDewar={onRefillDewar} // Pass the refill handler to Slot component
/> />
))} ))}
</StorageWrapper> </StorageWrapper>
<Button
variant="contained"
color="secondary"
onClick={() => {
const selectedSlotData = slotsData.find(slot => slot.qr_code === selectedSlot);
if (selectedSlotData) {
onRefillDewar(selectedSlotData);
} else {
alert('Please select a slot to refill its dewar.');
}
}}>
Refill Dewar
</Button>
</StorageContainer> </StorageContainer>
); );
}; };