
Introduced a new `pgroups` attribute for dewars in the backend with schema and model updates. Modified the frontend to display `pgroups` as chips, integrate new visual icons for pucks and crystals, and enhance the UI/UX in `ShipmentDetails` and `DewarStepper` components. Added reusable SVG components for better modularity and design consistency.
239 lines
10 KiB
TypeScript
239 lines
10 KiB
TypeScript
import React, { useState } from 'react';
|
|
import {Stepper, Step, StepLabel, Typography, Menu, MenuItem, IconButton, Box} from '@mui/material';
|
|
import AirplanemodeActiveIcon from '@mui/icons-material/AirplanemodeActive';
|
|
import StoreIcon from '@mui/icons-material/Store';
|
|
import RecycleIcon from '@mui/icons-material/Restore';
|
|
import AirplaneIcon from '@mui/icons-material/AirplanemodeActive';
|
|
import { Dewar, DewarsService } from "../../openapi";
|
|
import { DewarStatus, getStatusStepIndex, determineIconColor } from './statusUtils';
|
|
import Tooltip from "@mui/material/Tooltip";
|
|
import './DewarStepper.css';
|
|
|
|
const ICON_STYLE = { width: 24, height: 24 };
|
|
|
|
// Custom SVG Icon Component
|
|
const BottleIcon: React.FC<{ fill: string }> = ({ fill }) => (
|
|
<svg fill={fill} height="24px" width="24px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 276.777 276.777">
|
|
<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>
|
|
);
|
|
|
|
// Icons Mapping
|
|
const ICONS: {
|
|
[key: number]: (props?: React.ComponentProps<typeof AirplanemodeActiveIcon>) => React.ReactElement;
|
|
} = {
|
|
0: (props) => <BottleIcon fill="grey" {...props} />,
|
|
1: (props) => (
|
|
<AirplanemodeActiveIcon
|
|
style={{ ...ICON_STYLE, color: 'blue', cursor: 'pointer' }}
|
|
{...props} // Explicitly typing props
|
|
/>
|
|
),
|
|
2: (props) => <StoreIcon style={ICON_STYLE} {...props} />,
|
|
3: (props) => <RecycleIcon style={ICON_STYLE} {...props} />,
|
|
4: (props) => (
|
|
<AirplanemodeActiveIcon
|
|
style={{ ...ICON_STYLE, cursor: 'pointer' }}
|
|
color="success" // Use one of the predefined color keywords
|
|
{...props}
|
|
/>
|
|
),
|
|
5: (props) => (
|
|
<AirplanemodeActiveIcon
|
|
style={{ ...ICON_STYLE, cursor: 'pointer' }}
|
|
color="warning" // Use predefined keyword for color
|
|
{...props}
|
|
/>
|
|
),
|
|
};
|
|
// StepIconContainer Component
|
|
interface StepIconContainerProps {
|
|
completed?: boolean;
|
|
active?: boolean;
|
|
error?: boolean;
|
|
children?: React.ReactNode;
|
|
}
|
|
|
|
const StepIconContainer: React.FC<StepIconContainerProps> = ({
|
|
completed,
|
|
active,
|
|
error,
|
|
children,
|
|
}) => {
|
|
const className = [
|
|
completed ? 'completed' : '',
|
|
active ? 'active' : '',
|
|
error ? 'error' : '',
|
|
]
|
|
.filter(Boolean)
|
|
.join(' ');
|
|
|
|
return (
|
|
<div className={className}>{children}</div>
|
|
);
|
|
};
|
|
|
|
const StepIconComponent: React.FC<StepIconComponentProps> = ({
|
|
icon,
|
|
dewar,
|
|
isSelected,
|
|
refreshShipments,
|
|
...rest
|
|
}) => {
|
|
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
|
|
|
const handleMenuOpen = (event: React.MouseEvent<HTMLDivElement>) => {
|
|
// Trigger menu ONLY for the BottleIcon (icon === 0)
|
|
if (icon === 0) {
|
|
setAnchorEl(event.currentTarget);
|
|
}
|
|
};
|
|
|
|
const handleMenuClose = () => {
|
|
setAnchorEl(null);
|
|
};
|
|
|
|
const handleStatusChange = async (status: DewarStatus) => {
|
|
try {
|
|
const today = new Date().toISOString().split('T')[0];
|
|
const payload = {
|
|
dewar_id: dewar.id,
|
|
dewar_name: dewar.dewar_name,
|
|
tracking_number: dewar.tracking_number,
|
|
number_of_pucks: dewar.number_of_pucks,
|
|
number_of_samples: dewar.number_of_samples,
|
|
status: status,
|
|
ready_date: status === 'Ready for Shipping' ? today : null,
|
|
shipping_date: dewar.shipping_date,
|
|
arrival_date: dewar.arrival_date,
|
|
returning_date: dewar.returning_date,
|
|
qrcode: dewar.qrcode,
|
|
return_address_id: dewar.return_address_id,
|
|
contact_id: dewar.contact_id,
|
|
};
|
|
|
|
await DewarsService.updateDewarDewarsDewarIdPut(dewar.id, payload);
|
|
setAnchorEl(null);
|
|
refreshShipments();
|
|
} catch (error) {
|
|
console.error('Failed to update dewar status:', error);
|
|
alert('Failed to update dewar status. Please try again.');
|
|
}
|
|
};
|
|
|
|
const { iconIndex, color } = getIconProperties(icon, dewar);
|
|
|
|
return (
|
|
<div
|
|
onMouseEnter={icon === 0 ? handleMenuOpen : undefined} // Open menu for BottleIcon
|
|
onMouseLeave={icon === 0 ? handleMenuClose : undefined} // Close menu when leaving BottleIcon
|
|
style={{ position: 'relative', cursor: 'pointer' }} // "Button-like" cursor for all icons
|
|
{...rest}
|
|
>
|
|
<StepIconContainer
|
|
completed={rest['aria-activedescendant'] ? true : undefined}
|
|
active={Boolean(rest['aria-activedescendant'])}
|
|
error={rest.role === 'error'}
|
|
>
|
|
<Tooltip
|
|
title={icon === 1 ? `Tracking Number: ${dewar.tracking_number}` : ''} // Tooltip for Airplane icon
|
|
arrow
|
|
>
|
|
{ICONS[iconIndex]?.({
|
|
style: iconIndex === 0 ? { fill: color } : undefined,
|
|
}) ?? <Typography variant="body2" color="error">Invalid icon</Typography>}
|
|
</Tooltip>
|
|
</StepIconContainer>
|
|
|
|
{icon === 0 && (
|
|
<Menu
|
|
anchorEl={anchorEl}
|
|
open={Boolean(anchorEl)}
|
|
onClose={handleMenuClose}
|
|
MenuListProps={{
|
|
onMouseEnter: () => setAnchorEl(anchorEl), // Keep menu open on hover
|
|
onMouseLeave: handleMenuClose, // Close menu when leaving
|
|
}}
|
|
>
|
|
{['In Preparation', 'Ready for Shipping'].map((status) => (
|
|
<MenuItem key={status} onClick={() => handleStatusChange(status as DewarStatus)}>
|
|
{status}
|
|
</MenuItem>
|
|
))}
|
|
</Menu>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
// Icon properties retrieval based on the status and icon number
|
|
const getIconProperties = (icon: number, dewar: Dewar) => {
|
|
const status = dewar.status as DewarStatus;
|
|
const iconIndex = status === 'Delayed' && icon === 1 ? 5 : icon;
|
|
const color = determineIconColor(icon, status);
|
|
return { iconIndex, color };
|
|
};
|
|
|
|
// Steps of the stepper
|
|
const steps = ['In-House', 'Transit', 'At SLS', 'Returned'];
|
|
|
|
// Props for the CustomStepper
|
|
type CustomStepperProps = {
|
|
dewar: Dewar;
|
|
selectedDewarId: number | null;
|
|
refreshShipments: () => void;
|
|
};
|
|
|
|
// CustomStepper Component
|
|
const CustomStepper: React.FC<CustomStepperProps> = ({ dewar, selectedDewarId, refreshShipments }) => {
|
|
const activeStep = getStatusStepIndex(dewar.status as DewarStatus);
|
|
const isSelected = dewar.id === selectedDewarId;
|
|
|
|
return (
|
|
<div style={{ display: 'flex', justifyContent: 'center', width: '100%', maxWidth: '600px', margin: '0 auto' }}>
|
|
<Stepper alternativeLabel activeStep={activeStep} style={{ width: '100%' }}>
|
|
{steps.map((label, index) => (
|
|
<Step key={label}>
|
|
<StepLabel
|
|
StepIconComponent={(stepProps) => (
|
|
<StepIconComponent
|
|
{...stepProps}
|
|
icon={index}
|
|
dewar={dewar}
|
|
isSelected={isSelected}
|
|
refreshShipments={refreshShipments}
|
|
/>
|
|
)}
|
|
>
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: 1, // Space between text and icon if applied
|
|
}}
|
|
>
|
|
{/* Step label */}
|
|
<Typography variant="body2">
|
|
{label}
|
|
</Typography>
|
|
</Box>
|
|
{/* Optional: Date below the step */}
|
|
<Typography variant="body2">
|
|
{index === 0
|
|
? dewar.ready_date
|
|
: index === 1
|
|
? dewar.shipping_date
|
|
: index === 2
|
|
? dewar.arrival_date
|
|
: index === 3
|
|
? dewar.returning_date
|
|
: ''}
|
|
</Typography>
|
|
</StepLabel>
|
|
</Step>
|
|
))}
|
|
</Stepper>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default CustomStepper; |