Refactor slot UI and backend refill logic.
Updated slot styling for improved user feedback and responsiveness. Simplified LN2 representation with a new level bar and adjusted refill logic to a 48-hour interval. Removed unused functions for cleaner backend code.
This commit is contained in:
parent
6ff9cbe327
commit
27d2717a05
@ -1,4 +1,3 @@
|
|||||||
from datetime import timedelta
|
|
||||||
from app.models import Slot
|
from app.models import Slot
|
||||||
|
|
||||||
slotQRCodes = [
|
slotQRCodes = [
|
||||||
@ -55,14 +54,6 @@ slotQRCodes = [
|
|||||||
"Outgoing X06SA",
|
"Outgoing X06SA",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def timedelta_to_str(td: timedelta) -> str:
|
|
||||||
days, seconds = td.days, td.seconds
|
|
||||||
hours = days * 24 + seconds // 3600
|
|
||||||
minutes = (seconds % 172800) // 60
|
|
||||||
return f"PT{hours}H{minutes}M"
|
|
||||||
|
|
||||||
|
|
||||||
slots = [
|
slots = [
|
||||||
Slot(
|
Slot(
|
||||||
id=str(i + 1), # Convert id to string to match your schema
|
id=str(i + 1), # Convert id to string to match your schema
|
||||||
|
@ -19,7 +19,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
def calculate_time_until_refill(
|
def calculate_time_until_refill(
|
||||||
last_refill: Optional[datetime], refill_interval_hours: int = 1
|
last_refill: Optional[datetime], refill_interval_hours: int = 48
|
||||||
) -> int:
|
) -> int:
|
||||||
refill_interval = timedelta(hours=refill_interval_hours)
|
refill_interval = timedelta(hours=refill_interval_hours)
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React from 'react';
|
||||||
import { Box, Typography, Button, Alert } from '@mui/material';
|
import { Box, Typography, Button, Alert } from '@mui/material';
|
||||||
import styled, { keyframes, css } from 'styled-components';
|
import styled, { keyframes, css } from 'styled-components';
|
||||||
import LocalGasStationIcon from '@mui/icons-material/LocalGasStation';
|
|
||||||
import LocationOnIcon from '@mui/icons-material/LocationOn';
|
import LocationOnIcon from '@mui/icons-material/LocationOn';
|
||||||
import CountdownTimer from './CountdownTimer';
|
import CountdownTimer from './CountdownTimer';
|
||||||
|
|
||||||
@ -43,19 +42,16 @@ const pulse = keyframes`
|
|||||||
const StyledSlot = styled(Box)<{ isSelected: boolean; isOccupied: boolean; atBeamline: boolean; isRetrieved: boolean; needsRefillSoon: boolean }>`
|
const StyledSlot = styled(Box)<{ isSelected: boolean; isOccupied: boolean; atBeamline: boolean; isRetrieved: boolean; needsRefillSoon: boolean }>`
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
margin: 8px;
|
margin: 8px;
|
||||||
width: 150px;
|
width: 140px;
|
||||||
height: 260px;
|
height: 220px;
|
||||||
background-color: ${({ isSelected, isOccupied, atBeamline, isRetrieved }) => {
|
background-color: ${({ isSelected, isOccupied, atBeamline }) => {
|
||||||
if (isSelected) {
|
|
||||||
return '#3f51b5';
|
|
||||||
}
|
|
||||||
if (isRetrieved && !atBeamline) {
|
|
||||||
return '#9e9e9e'; // Grey color for retrieved but not at beamline
|
|
||||||
}
|
|
||||||
if (atBeamline) {
|
if (atBeamline) {
|
||||||
return '#ff9800';
|
return isSelected ? '#cc7a00' : '#ff9800'; // Darker orange if selected
|
||||||
}
|
}
|
||||||
return isOccupied ? '#f44336' : '#4caf50';
|
if (isOccupied) {
|
||||||
|
return isSelected ? '#d32f2f' : '#f44336'; // Darker red if selected
|
||||||
|
}
|
||||||
|
return isSelected ? '#5a9e5c' : '#88bc8a'; // Darker green if selected
|
||||||
}};
|
}};
|
||||||
color: white;
|
color: white;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -65,9 +61,17 @@ const StyledSlot = styled(Box)<{ isSelected: boolean; isOccupied: boolean; atBea
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.1);
|
box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
transition: transform 0.2s;
|
transition: transform 0.2s, background-color 0.2s;
|
||||||
|
|
||||||
|
${({ isSelected }) =>
|
||||||
|
isSelected &&
|
||||||
|
css`
|
||||||
|
border: 3px solid #3f51b5;
|
||||||
|
`}
|
||||||
|
/* Pulsing animation for occupied slots needing refill */
|
||||||
${({ isOccupied, needsRefillSoon }) =>
|
${({ isOccupied, needsRefillSoon }) =>
|
||||||
isOccupied && needsRefillSoon &&
|
isOccupied &&
|
||||||
|
needsRefillSoon &&
|
||||||
css`
|
css`
|
||||||
animation: ${pulse} 1.5s infinite;
|
animation: ${pulse} 1.5s infinite;
|
||||||
`}
|
`}
|
||||||
@ -77,29 +81,34 @@ const StyledSlot = styled(Box)<{ isSelected: boolean; isOccupied: boolean; atBea
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const BottleIcon: React.FC<{ fillHeight: number }> = ({ fillHeight }) => {
|
const BottleIcon: React.FC<{ fill: string }> = ({ fill }) => (
|
||||||
const pixelHeight = (276.777 * fillHeight) / 100;
|
<svg fill={fill} height="48px" width="48px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 276.777 276.777">
|
||||||
const yPosition = 276.777 - pixelHeight;
|
<path d="M190.886,82.273c-3.23-2.586-7.525-7.643-7.525-21.639V43h8.027V0h-106v43h8.027v17.635c0,11.66-1.891,17.93-6.524,21.639 c-21.813,17.459-31.121,36.748-31.121,64.5v100.088c0,16.496,13.42,29.916,29.916,29.916h105.405 c16.496,0,29.916-13.42,29.916-29.916V146.773C221.007,121.103,210.029,97.594,190.886,82.273z M191.007,246.777H85.77V146.773 c0-18.589,5.199-29.339,19.867-41.078c15.758-12.612,17.778-30.706,17.778-45.061V43h29.945v17.635 c0,19.927,6.318,35.087,18.779,45.061c11.99,9.597,18.867,24.568,18.867,41.078V246.777z"/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const LN2LevelBar: React.FC<{ level: number }> = ({ level }) => {
|
||||||
|
const barHeight = `${level}%`; // Proportional height based on LN2 level
|
||||||
|
const barColor = level > 50 ? '#00aeed' : level > 20 ? '#ff9800' : '#f44336'; // Green > Yellow > Red
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg height="100px" width="50px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 276.777 276.777">
|
<Box
|
||||||
<defs>
|
width="7px"
|
||||||
<clipPath id="bottle-clip">
|
height="100%"
|
||||||
<path
|
border="1px solid lightgray"
|
||||||
d="M190.886,82.273c-3.23-2.586-7.525-7.643-7.525-21.639V43h8.027V0h-106v43h8.027v17.635
|
display="flex"
|
||||||
c0,11.66-1.891,17.93-6.524,21.639c-21.813,17.459-31.121,36.748-31.121,64.5v100.088c0,16.496,13.42,29.916,29.916,29.916
|
alignItems="center"
|
||||||
h105.405c16.496,0,29.916-13.42,29.916-29.916V146.773C221.007,121.103,210.029,97.594,190.886,82.273z"
|
justifyContent="flex-end" // Align contents to the bottom
|
||||||
/>
|
flexDirection="column" // Needed for vertical alignment
|
||||||
</clipPath>
|
>
|
||||||
</defs>
|
{/* The filled portion of the bar */}
|
||||||
<path
|
<Box
|
||||||
fill="lightgray"
|
width="100%"
|
||||||
d="M190.886,82.273c-3.23-2.586-7.525-7.643-7.525-21.639V43h8.027V0h-106v43h8.027v17.635
|
height={barHeight} // Set height proportional to the level
|
||||||
c0,11.66-1.891,17.93-6.524,21.639c-21.813,17.459-31.121,36.748-31.121,64.5v100.088c0,16.496,13.42,29.916,29.916,29.916
|
bgcolor={barColor}
|
||||||
h105.405c16.496,0,29.916-13.42,29.916-29.916V146.773C221.007,121.103,210.029,97.594,190.886,82.273z"
|
transition="height 0.3s ease" // Smooth transition when level changes
|
||||||
/>
|
/>
|
||||||
<rect x="0" y={yPosition} width="100%" height={pixelHeight} fill="#00bfff" clipPath="url(#bottle-clip)" />
|
</Box>
|
||||||
</svg>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -108,20 +117,14 @@ const Slot: React.FC<SlotProps> = ({ data, isSelected, onSelect, onRefillDewar,
|
|||||||
|
|
||||||
const calculateFillHeight = (timeUntilRefill?: number) => {
|
const calculateFillHeight = (timeUntilRefill?: number) => {
|
||||||
if (timeUntilRefill === undefined || timeUntilRefill <= 0) {
|
if (timeUntilRefill === undefined || timeUntilRefill <= 0) {
|
||||||
return 0;
|
return 0; // Return 0% if time is undefined or negative
|
||||||
}
|
}
|
||||||
const maxTime = 3600; // Example maximum time for calculating fill height
|
const maxTime = 172800; // Example maximum time (2 days in seconds)
|
||||||
return Math.min((timeUntilRefill / maxTime) * 100, 100);
|
return Math.min((timeUntilRefill / maxTime) * 100, 100);
|
||||||
};
|
};
|
||||||
|
|
||||||
const fillHeight = calculateFillHeight(time_until_refill);
|
const fillHeight = calculateFillHeight(time_until_refill);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (time_until_refill !== undefined) {
|
|
||||||
console.log(`Updated time_until_refill: ${time_until_refill}`);
|
|
||||||
}
|
|
||||||
}, [time_until_refill]);
|
|
||||||
|
|
||||||
const handleRefill = async () => {
|
const handleRefill = async () => {
|
||||||
if (dewar_unique_id) {
|
if (dewar_unique_id) {
|
||||||
await onRefillDewar(dewar_unique_id);
|
await onRefillDewar(dewar_unique_id);
|
||||||
@ -132,7 +135,6 @@ const Slot: React.FC<SlotProps> = ({ data, isSelected, onSelect, onRefillDewar,
|
|||||||
const isSpecificBeamline = beamlineLocation === 'X10SA' || beamlineLocation === 'X06SA' || beamlineLocation === 'X06DA';
|
const isSpecificBeamline = beamlineLocation === 'X10SA' || beamlineLocation === 'X06SA' || beamlineLocation === 'X06DA';
|
||||||
const isRetrieved = retrieved === true && !isSpecificBeamline;
|
const isRetrieved = retrieved === true && !isSpecificBeamline;
|
||||||
|
|
||||||
// Consider slot needs a refill soon if it's occupied and time_until_refill is less than 3 hours (or 0)
|
|
||||||
const needsRefillSoon = occupied && (time_until_refill !== undefined && time_until_refill <= 10800);
|
const needsRefillSoon = occupied && (time_until_refill !== undefined && time_until_refill <= 10800);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -140,30 +142,93 @@ const Slot: React.FC<SlotProps> = ({ data, isSelected, onSelect, onRefillDewar,
|
|||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
isOccupied={occupied}
|
isOccupied={occupied}
|
||||||
atBeamline={isSpecificBeamline}
|
atBeamline={isSpecificBeamline}
|
||||||
isRetrieved={isRetrieved} // prop to control slot color for retrieved
|
isRetrieved={isRetrieved}
|
||||||
needsRefillSoon={needsRefillSoon} // prop to control pulsing animation
|
needsRefillSoon={needsRefillSoon}
|
||||||
onClick={() => onSelect(data)}
|
onClick={() => onSelect(data)}
|
||||||
>
|
>
|
||||||
<Typography variant="h6">{label}</Typography>
|
<Box
|
||||||
{dewar_name && <Typography variant="body2">{`${dewar_name}`}</Typography>}
|
display="flex"
|
||||||
{needs_refill && <LocalGasStationIcon />}
|
alignItems="stretch"
|
||||||
{dewar_unique_id && <BottleIcon fillHeight={fillHeight} />}
|
height="100%"
|
||||||
{isSpecificBeamline && (
|
width="100%"
|
||||||
<Typography style={{ fontSize: 12 }}>
|
>
|
||||||
<LocationOnIcon /> {beamlineLocation}
|
{/* LN2 Level Bar */}
|
||||||
</Typography>
|
{dewar_unique_id && (
|
||||||
)}
|
<Box width="10px" height="100%" marginRight="8px">
|
||||||
{(dewar_unique_id && time_until_refill !== undefined && time_until_refill !== -1) ? (
|
<LN2LevelBar level={fillHeight} />
|
||||||
<CountdownTimer key={dewar_unique_id} totalSeconds={time_until_refill} />
|
</Box>
|
||||||
) : null}
|
)}
|
||||||
{needs_refill && (
|
|
||||||
<Button onClick={handleRefill} sx={{ mt: 1, color: 'white' }}>
|
{/* Main Content */}
|
||||||
Refill
|
<Box
|
||||||
</Button>
|
flexGrow={1}
|
||||||
)}
|
display="flex"
|
||||||
{occupied && time_until_refill === -1 && (
|
flexDirection="column"
|
||||||
<Alert severity="warning">This dewar has no recorded refill event. It needs to be refilled.</Alert>
|
alignItems="center"
|
||||||
)}
|
justifyContent="space-between"
|
||||||
|
>
|
||||||
|
{/* Top-Centered Text */}
|
||||||
|
<Box textAlign="center" width="100%">
|
||||||
|
<Typography variant="h6">{label}</Typography>
|
||||||
|
{dewar_name && (
|
||||||
|
<Typography variant="body2">
|
||||||
|
{dewar_name}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Beamline Location Icon & Bottle Icon Next to Each Other */}
|
||||||
|
<Box
|
||||||
|
display="flex"
|
||||||
|
flexDirection="column"
|
||||||
|
alignItems="center"
|
||||||
|
marginTop="8px"
|
||||||
|
marginBottom="16px"
|
||||||
|
>
|
||||||
|
{/* Row for Icons */}
|
||||||
|
<Box display="flex" flexDirection="row" alignItems="center" gap="8px">
|
||||||
|
{isSpecificBeamline && (
|
||||||
|
<Box display="flex" flexDirection="column" alignItems="center">
|
||||||
|
<LocationOnIcon style={{ fontSize: '48px', color: '#37ff00' }} />
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
style={{
|
||||||
|
fontWeight: 'bold',
|
||||||
|
marginTop: '4px',
|
||||||
|
textAlign: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{beamlineLocation}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{occupied && (
|
||||||
|
<BottleIcon fill="#ffffff" />
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Refill Button */}
|
||||||
|
{needs_refill && (
|
||||||
|
<Button onClick={handleRefill} sx={{ mt: 1, color: 'white' }}>
|
||||||
|
Refill
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Countdown Timer */}
|
||||||
|
{dewar_unique_id && time_until_refill !== undefined && time_until_refill !== -1 ? (
|
||||||
|
<CountdownTimer key={dewar_unique_id} totalSeconds={time_until_refill} />
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{/* Warning */}
|
||||||
|
{occupied && time_until_refill === -1 && (
|
||||||
|
<Alert severity="warning">
|
||||||
|
This dewar has no recorded refill event. It needs to
|
||||||
|
be refilled.
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
</StyledSlot>
|
</StyledSlot>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -13,7 +13,7 @@ const StorageContainer = styled(Box)`
|
|||||||
|
|
||||||
const StorageWrapper = styled.div`
|
const StorageWrapper = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap; /* Enable rows when wrapping */
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 90%;
|
width: 90%;
|
||||||
background-color: #dcdcdc;
|
background-color: #dcdcdc;
|
||||||
@ -21,6 +21,12 @@ const StorageWrapper = styled.div`
|
|||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
/* Media query for narrow screens */
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px; /* Adjust padding for phones */
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface StorageProps {
|
interface StorageProps {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user