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 '',
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)
]

View File

@ -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"

View File

@ -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"}

View File

@ -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

View File

@ -9,12 +9,14 @@ const CountdownTimer: React.FC<CountdownTimerProps> = ({ 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<CountdownTimerProps> = ({ totalSeconds }) => {
return `${hrs}h ${min}m ${sec}s`;
};
const timeColor = timeLeft < 300 ? 'red' : 'white'; // Red if less than 5 minutes remaining
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)}`}
</Typography>
);

View File

@ -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<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;
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<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 (
<StyledSlot
isSelected={isSelected}
isOccupied={data.occupied}
isOccupied={occupied}
onClick={() => onSelect(data)}
{...rest}
>
<Typography variant="h6">{data.label}</Typography>
{data.dewar_name && (
<Typography variant="body2">{`Dewar: ${data.dewar_name}`}</Typography>
<Typography variant="h6">{label}</Typography>
{dewar_name && <Typography variant="body2">{`Dewar: ${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 && (
<Typography variant="body2">{`ID: ${data.dewar_unique_id}`}</Typography>
{needs_refill && (
<Button onClick={handleRefill} sx={{ mt: 1, color: 'white' }}>
Refill
</Button>
)}
{data.needs_refill && <LocalGasStationIcon />}
{/* Display countdown timer only for slots that have an associated dewar */}
{data.dewar_unique_id && data.time_until_refill !== undefined && (
<CountdownTimer totalSeconds={data.time_until_refill} />
{occupied && time_until_refill === -1 && (
<Alert severity="warning">This dewar has no recorded refill event. It needs to be refilled.</Alert>
)}
</StyledSlot>
);

View File

@ -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<StorageProps> = ({ name, selectedSlot, slotsData, onSelectSlot }) => {
const Storage: React.FC<StorageProps> = ({ name, selectedSlot, slotsData, onSelectSlot, onRefillDewar }) => {
const handleSlotSelect = (slot: SlotData) => {
onSelectSlot(slot);
console.log('Selected slot:', slot);
@ -46,9 +47,23 @@ const Storage: React.FC<StorageProps> = ({ name, selectedSlot, slotsData, onSele
data={slot}
onSelect={handleSlotSelect}
isSelected={selectedSlot === slot.qr_code}
onRefillDewar={onRefillDewar} // Pass the refill handler to Slot component
/>
))}
</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>
);
};