Add pgroup handling in dewars and enhance ShipmentDetails UI
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.
This commit is contained in:
parent
173e192fc4
commit
44582cf38e
@ -189,6 +189,7 @@ def generate_unique_id(length=16):
|
|||||||
dewars = [
|
dewars = [
|
||||||
Dewar(
|
Dewar(
|
||||||
id=1,
|
id=1,
|
||||||
|
pgroups="p20001, p20002",
|
||||||
dewar_name="Dewar One",
|
dewar_name="Dewar One",
|
||||||
dewar_type_id=1,
|
dewar_type_id=1,
|
||||||
dewar_serial_number_id=2,
|
dewar_serial_number_id=2,
|
||||||
@ -204,6 +205,7 @@ dewars = [
|
|||||||
),
|
),
|
||||||
Dewar(
|
Dewar(
|
||||||
id=2,
|
id=2,
|
||||||
|
pgroups="p20001, p20002",
|
||||||
dewar_name="Dewar Two",
|
dewar_name="Dewar Two",
|
||||||
dewar_type_id=3,
|
dewar_type_id=3,
|
||||||
dewar_serial_number_id=1,
|
dewar_serial_number_id=1,
|
||||||
@ -219,6 +221,7 @@ dewars = [
|
|||||||
),
|
),
|
||||||
Dewar(
|
Dewar(
|
||||||
id=3,
|
id=3,
|
||||||
|
pgroups="p20004",
|
||||||
dewar_name="Dewar Three",
|
dewar_name="Dewar Three",
|
||||||
dewar_type_id=2,
|
dewar_type_id=2,
|
||||||
dewar_serial_number_id=3,
|
dewar_serial_number_id=3,
|
||||||
@ -234,6 +237,7 @@ dewars = [
|
|||||||
),
|
),
|
||||||
Dewar(
|
Dewar(
|
||||||
id=4,
|
id=4,
|
||||||
|
pgroups="p20004",
|
||||||
dewar_name="Dewar Four",
|
dewar_name="Dewar Four",
|
||||||
dewar_type_id=2,
|
dewar_type_id=2,
|
||||||
dewar_serial_number_id=4,
|
dewar_serial_number_id=4,
|
||||||
@ -249,6 +253,7 @@ dewars = [
|
|||||||
),
|
),
|
||||||
Dewar(
|
Dewar(
|
||||||
id=5,
|
id=5,
|
||||||
|
pgroups="p20001, p20002",
|
||||||
dewar_name="Dewar Five",
|
dewar_name="Dewar Five",
|
||||||
dewar_type_id=1,
|
dewar_type_id=1,
|
||||||
dewar_serial_number_id=1,
|
dewar_serial_number_id=1,
|
||||||
|
@ -80,13 +80,14 @@ class Dewar(Base):
|
|||||||
__tablename__ = "dewars"
|
__tablename__ = "dewars"
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||||
dewar_name = Column(String(255))
|
pgroups = Column(String(255), nullable=False)
|
||||||
|
dewar_name = Column(String(255), nullable=False)
|
||||||
dewar_type_id = Column(Integer, ForeignKey("dewar_types.id"), nullable=True)
|
dewar_type_id = Column(Integer, ForeignKey("dewar_types.id"), nullable=True)
|
||||||
dewar_serial_number_id = Column(
|
dewar_serial_number_id = Column(
|
||||||
Integer, ForeignKey("dewar_serial_numbers.id"), nullable=True
|
Integer, ForeignKey("dewar_serial_numbers.id"), nullable=True
|
||||||
)
|
)
|
||||||
tracking_number = Column(String(255))
|
tracking_number = Column(String(255), nullable=True)
|
||||||
status = Column(String(255))
|
status = Column(String(255), nullable=True)
|
||||||
ready_date = Column(Date, nullable=True)
|
ready_date = Column(Date, nullable=True)
|
||||||
shipping_date = Column(Date, nullable=True)
|
shipping_date = Column(Date, nullable=True)
|
||||||
arrival_date = Column(Date, nullable=True)
|
arrival_date = Column(Date, nullable=True)
|
||||||
|
@ -523,6 +523,7 @@ class DewarCreate(DewarBase):
|
|||||||
|
|
||||||
class Dewar(DewarBase):
|
class Dewar(DewarBase):
|
||||||
id: int
|
id: int
|
||||||
|
pgroups: str
|
||||||
shipment_id: Optional[int]
|
shipment_id: Optional[int]
|
||||||
contact: Optional[Contact]
|
contact: Optional[Contact]
|
||||||
return_address: Optional[Address]
|
return_address: Optional[Address]
|
||||||
|
18
frontend/src/assets/icons/CrystalIcon.tsx
Normal file
18
frontend/src/assets/icons/CrystalIcon.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const CrystalFacetedIcon: React.FC<{ size?: number; color?: string }> = ({ size = 50, color = "#28A745" }) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill={color}
|
||||||
|
>
|
||||||
|
{/* Facets */}
|
||||||
|
<polygon points="12,2 19,9.5 12,22 5,9.5" fill={color} />
|
||||||
|
<polygon points="12,2 5,9.5 19,9.5" fill="#e3f2fd" />
|
||||||
|
<polygon points="12,22 19,9.5 5,9.5" fill="rgba(0,0,0,0.1)" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default CrystalFacetedIcon;
|
46
frontend/src/assets/icons/SimplePuckIcon.tsx
Normal file
46
frontend/src/assets/icons/SimplePuckIcon.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Box, Typography } from "@mui/material";
|
||||||
|
|
||||||
|
// Define the props interface for PuckDetailsVisual
|
||||||
|
interface PuckDetailsVisualProps {
|
||||||
|
puckCount: number; // Total number of pucks
|
||||||
|
}
|
||||||
|
|
||||||
|
// This component purely represents a simple puck icon with 16 filled black circles
|
||||||
|
const SimplePuckIcon: React.FC = () => (
|
||||||
|
<svg width="50" height="50" viewBox="0 0 100 100">
|
||||||
|
<circle cx="50" cy="50" r="45" stroke="black" strokeWidth="2" fill="none" />
|
||||||
|
{[...Array(11)].map((_, index) => {
|
||||||
|
const angle = (index * (360 / 11)) * (Math.PI / 180);
|
||||||
|
const x = 50 + 35 * Math.cos(angle);
|
||||||
|
const y = 50 + 35 * Math.sin(angle);
|
||||||
|
return <circle key={index} cx={x} cy={y} r="5" fill="black" />;
|
||||||
|
})}
|
||||||
|
{[...Array(5)].map((_, index) => {
|
||||||
|
const angle = (index * (360 / 5) + 36) * (Math.PI / 180);
|
||||||
|
const x = 50 + 15 * Math.cos(angle);
|
||||||
|
const y = 50 + 15 * Math.sin(angle);
|
||||||
|
return <circle key={index + 11} cx={x} cy={y} r="5" fill="black" />;
|
||||||
|
})}
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
// A wrapper component for displaying the puck icon and the count
|
||||||
|
export const PuckDetailsVisual: React.FC<PuckDetailsVisualProps> = ({ puckCount }) => {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SimplePuckIcon />
|
||||||
|
<Typography variant="body1" fontWeight="bold">
|
||||||
|
{puckCount}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SimplePuckIcon; // Only SimplePuckIcon will be the default export
|
14
frontend/src/components/DewarStepper.css
Normal file
14
frontend/src/components/DewarStepper.css
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
.completed {
|
||||||
|
background-color: #e0ffe0;
|
||||||
|
cursor: pointer; /* Ensure pointer is enabled */
|
||||||
|
}
|
||||||
|
|
||||||
|
.active {
|
||||||
|
background-color: #f0f8ff;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
background-color: #ffe0e0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
@ -1,10 +1,13 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Stepper, Step, StepLabel, Typography, Menu, MenuItem } from '@mui/material';
|
import {Stepper, Step, StepLabel, Typography, Menu, MenuItem, IconButton, Box} from '@mui/material';
|
||||||
import AirplanemodeActiveIcon from '@mui/icons-material/AirplanemodeActive';
|
import AirplanemodeActiveIcon from '@mui/icons-material/AirplanemodeActive';
|
||||||
import StoreIcon from '@mui/icons-material/Store';
|
import StoreIcon from '@mui/icons-material/Store';
|
||||||
import RecycleIcon from '@mui/icons-material/Restore';
|
import RecycleIcon from '@mui/icons-material/Restore';
|
||||||
|
import AirplaneIcon from '@mui/icons-material/AirplanemodeActive';
|
||||||
import { Dewar, DewarsService } from "../../openapi";
|
import { Dewar, DewarsService } from "../../openapi";
|
||||||
import { DewarStatus, getStatusStepIndex, determineIconColor } from './statusUtils';
|
import { DewarStatus, getStatusStepIndex, determineIconColor } from './statusUtils';
|
||||||
|
import Tooltip from "@mui/material/Tooltip";
|
||||||
|
import './DewarStepper.css';
|
||||||
|
|
||||||
const ICON_STYLE = { width: 24, height: 24 };
|
const ICON_STYLE = { width: 24, height: 24 };
|
||||||
|
|
||||||
@ -16,15 +19,33 @@ const BottleIcon: React.FC<{ fill: string }> = ({ fill }) => (
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Icons Mapping
|
// Icons Mapping
|
||||||
const ICONS: { [key: number]: React.ReactElement } = {
|
const ICONS: {
|
||||||
0: <BottleIcon fill="grey" />,
|
[key: number]: (props?: React.ComponentProps<typeof AirplanemodeActiveIcon>) => React.ReactElement;
|
||||||
1: <AirplanemodeActiveIcon style={{ ...ICON_STYLE, color: 'blue' }} />,
|
} = {
|
||||||
2: <StoreIcon style={ICON_STYLE} />,
|
0: (props) => <BottleIcon fill="grey" {...props} />,
|
||||||
3: <RecycleIcon style={ICON_STYLE} />,
|
1: (props) => (
|
||||||
4: <AirplanemodeActiveIcon style={{ ...ICON_STYLE, color: 'green' }} />,
|
<AirplanemodeActiveIcon
|
||||||
5: <AirplanemodeActiveIcon style={{ ...ICON_STYLE, color: 'orange' }} />,
|
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
|
// StepIconContainer Component
|
||||||
interface StepIconContainerProps {
|
interface StepIconContainerProps {
|
||||||
completed?: boolean;
|
completed?: boolean;
|
||||||
@ -33,39 +54,42 @@ interface StepIconContainerProps {
|
|||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StepIconContainer: React.FC<StepIconContainerProps> = ({ completed, active, error, children }) => {
|
const StepIconContainer: React.FC<StepIconContainerProps> = ({
|
||||||
|
completed,
|
||||||
|
active,
|
||||||
|
error,
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
const className = [
|
const className = [
|
||||||
completed ? 'completed' : '',
|
completed ? 'completed' : '',
|
||||||
active ? 'active' : '',
|
active ? 'active' : '',
|
||||||
error ? 'error' : '',
|
error ? 'error' : '',
|
||||||
].join(' ').trim();
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>{children}</div>
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// StepIconComponent Props
|
const StepIconComponent: React.FC<StepIconComponentProps> = ({
|
||||||
type StepIconComponentProps = {
|
icon,
|
||||||
icon: number;
|
dewar,
|
||||||
dewar: Dewar;
|
isSelected,
|
||||||
isSelected: boolean;
|
refreshShipments,
|
||||||
refreshShipments: () => void;
|
...rest
|
||||||
} & Omit<React.HTMLAttributes<HTMLDivElement>, 'icon'>;
|
}) => {
|
||||||
|
|
||||||
// StepIconComponent
|
|
||||||
const StepIconComponent: React.FC<StepIconComponentProps> = ({ icon, dewar, isSelected, refreshShipments, ...rest }) => {
|
|
||||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||||
|
|
||||||
const handleIconEnter = (event: React.MouseEvent<HTMLDivElement>) => {
|
const handleMenuOpen = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||||
if (isSelected && icon === 0) {
|
// Trigger menu ONLY for the BottleIcon (icon === 0)
|
||||||
|
if (icon === 0) {
|
||||||
setAnchorEl(event.currentTarget);
|
setAnchorEl(event.currentTarget);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleIconLeave = () => {
|
const handleMenuClose = () => {
|
||||||
setAnchorEl(null);
|
setAnchorEl(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -98,13 +122,12 @@ const StepIconComponent: React.FC<StepIconComponentProps> = ({ icon, dewar, isSe
|
|||||||
};
|
};
|
||||||
|
|
||||||
const { iconIndex, color } = getIconProperties(icon, dewar);
|
const { iconIndex, color } = getIconProperties(icon, dewar);
|
||||||
const IconComponent = ICONS[iconIndex];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
onMouseEnter={handleIconEnter}
|
onMouseEnter={icon === 0 ? handleMenuOpen : undefined} // Open menu for BottleIcon
|
||||||
onMouseLeave={handleIconLeave}
|
onMouseLeave={icon === 0 ? handleMenuClose : undefined} // Close menu when leaving BottleIcon
|
||||||
style={{ position: 'relative' }}
|
style={{ position: 'relative', cursor: 'pointer' }} // "Button-like" cursor for all icons
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
<StepIconContainer
|
<StepIconContainer
|
||||||
@ -112,31 +135,36 @@ const StepIconComponent: React.FC<StepIconComponentProps> = ({ icon, dewar, isSe
|
|||||||
active={Boolean(rest['aria-activedescendant'])}
|
active={Boolean(rest['aria-activedescendant'])}
|
||||||
error={rest.role === 'error'}
|
error={rest.role === 'error'}
|
||||||
>
|
>
|
||||||
{IconComponent
|
<Tooltip
|
||||||
? React.cloneElement(IconComponent, iconIndex === 0 ? { fill: color } : {})
|
title={icon === 1 ? `Tracking Number: ${dewar.tracking_number}` : ''} // Tooltip for Airplane icon
|
||||||
: <Typography variant="body2" color="error">Invalid icon</Typography>
|
arrow
|
||||||
}
|
>
|
||||||
|
{ICONS[iconIndex]?.({
|
||||||
|
style: iconIndex === 0 ? { fill: color } : undefined,
|
||||||
|
}) ?? <Typography variant="body2" color="error">Invalid icon</Typography>}
|
||||||
|
</Tooltip>
|
||||||
</StepIconContainer>
|
</StepIconContainer>
|
||||||
|
|
||||||
<Menu
|
{icon === 0 && (
|
||||||
anchorEl={anchorEl}
|
<Menu
|
||||||
open={Boolean(anchorEl)}
|
anchorEl={anchorEl}
|
||||||
onClose={handleIconLeave}
|
open={Boolean(anchorEl)}
|
||||||
MenuListProps={{
|
onClose={handleMenuClose}
|
||||||
onMouseEnter: () => setAnchorEl(anchorEl),
|
MenuListProps={{
|
||||||
onMouseLeave: handleIconLeave,
|
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)}>
|
{['In Preparation', 'Ready for Shipping'].map((status) => (
|
||||||
{status}
|
<MenuItem key={status} onClick={() => handleStatusChange(status as DewarStatus)}>
|
||||||
</MenuItem>
|
{status}
|
||||||
))}
|
</MenuItem>
|
||||||
</Menu>
|
))}
|
||||||
|
</Menu>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Icon properties retrieval based on the status and icon number
|
// Icon properties retrieval based on the status and icon number
|
||||||
const getIconProperties = (icon: number, dewar: Dewar) => {
|
const getIconProperties = (icon: number, dewar: Dewar) => {
|
||||||
const status = dewar.status as DewarStatus;
|
const status = dewar.status as DewarStatus;
|
||||||
@ -166,16 +194,41 @@ const CustomStepper: React.FC<CustomStepperProps> = ({ dewar, selectedDewarId, r
|
|||||||
{steps.map((label, index) => (
|
{steps.map((label, index) => (
|
||||||
<Step key={label}>
|
<Step key={label}>
|
||||||
<StepLabel
|
<StepLabel
|
||||||
StepIconComponent={(stepProps) => <StepIconComponent {...stepProps} icon={index} dewar={dewar} isSelected={isSelected} refreshShipments={refreshShipments} />}
|
StepIconComponent={(stepProps) => (
|
||||||
|
<StepIconComponent
|
||||||
|
{...stepProps}
|
||||||
|
icon={index}
|
||||||
|
dewar={dewar}
|
||||||
|
isSelected={isSelected}
|
||||||
|
refreshShipments={refreshShipments}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{label}
|
<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>
|
</StepLabel>
|
||||||
<Typography variant="body2">
|
|
||||||
{index === 0 ? dewar.ready_date :
|
|
||||||
index === 1 ? dewar.shipping_date :
|
|
||||||
index === 2 ? dewar.arrival_date :
|
|
||||||
index === 3 ? dewar.returning_date : ''}
|
|
||||||
</Typography>
|
|
||||||
</Step>
|
</Step>
|
||||||
))}
|
))}
|
||||||
</Stepper>
|
</Stepper>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Box, Typography, Button, Stack, TextField, IconButton, Grid } from '@mui/material';
|
import {Box, Typography, Button, Stack, TextField, IconButton, Grid, Chip} from '@mui/material';
|
||||||
import QRCode from 'react-qr-code';
|
import QRCode from 'react-qr-code';
|
||||||
import DeleteIcon from "@mui/icons-material/Delete";
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
import CheckIcon from '@mui/icons-material/Check';
|
import CheckIcon from '@mui/icons-material/Check';
|
||||||
@ -8,6 +8,9 @@ import { Dewar, DewarsService, Shipment, Contact, ApiError, ShipmentsService } f
|
|||||||
import { SxProps } from "@mui/system";
|
import { SxProps } from "@mui/system";
|
||||||
import CustomStepper from "./DewarStepper";
|
import CustomStepper from "./DewarStepper";
|
||||||
import DewarDetails from './DewarDetails';
|
import DewarDetails from './DewarDetails';
|
||||||
|
import { PuckDetailsVisual } from '../assets/icons/SimplePuckIcon';
|
||||||
|
import CrystalFacetedIcon from "../assets/icons/CrystalIcon.tsx";
|
||||||
|
|
||||||
|
|
||||||
const MAX_COMMENTS_LENGTH = 200;
|
const MAX_COMMENTS_LENGTH = 200;
|
||||||
|
|
||||||
@ -183,6 +186,39 @@ const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
|
|||||||
const isCommentsEdited = comments !== initialComments;
|
const isCommentsEdited = comments !== initialComments;
|
||||||
const contact = selectedShipment?.contact;
|
const contact = selectedShipment?.contact;
|
||||||
|
|
||||||
|
const renderPgroupChips = () => {
|
||||||
|
// Safely handle pgroups as an array
|
||||||
|
const pgroupsArray = Array.isArray(selectedShipment?.pgroups)
|
||||||
|
? selectedShipment.pgroups
|
||||||
|
: selectedShipment?.pgroups?.split(",").map((pgroup: string) => pgroup.trim()) || [];
|
||||||
|
|
||||||
|
if (!pgroupsArray.length) {
|
||||||
|
return <Typography variant="body2">No associated pgroups</Typography>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pgroupsArray.map((pgroup: string) => (
|
||||||
|
<Chip
|
||||||
|
key={pgroup}
|
||||||
|
label={pgroup}
|
||||||
|
color={pgroup === activePgroup ? "primary" : "default"} // Highlight active pgroups
|
||||||
|
sx={{
|
||||||
|
margin: 0.5,
|
||||||
|
backgroundColor: pgroup === activePgroup ? '#19d238' : '#b0b0b0',
|
||||||
|
color: pgroup === activePgroup ? 'white' : 'black',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
borderRadius: '8px',
|
||||||
|
height: '20px',
|
||||||
|
fontSize: '12px',
|
||||||
|
boxShadow: '0px 1px 3px rgba(0, 0, 0, 0.2)',
|
||||||
|
//cursor: isAssociated ? 'default' : 'pointer', // Disable pointer for associated chips
|
||||||
|
//'&:hover': { opacity: isAssociated ? 1 : 0.8 }, // Disable hover effect for associated chips
|
||||||
|
mr: 1,
|
||||||
|
mb: 1,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ ...sx, padding: 2, textAlign: 'left' }}>
|
<Box sx={{ ...sx, padding: 2, textAlign: 'left' }}>
|
||||||
{!localSelectedDewar && !isAddingDewar && (
|
{!localSelectedDewar && !isAddingDewar && (
|
||||||
@ -229,6 +265,9 @@ const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
|
|||||||
<Grid item xs={12} md={6}>
|
<Grid item xs={12} md={6}>
|
||||||
<Box sx={{ marginTop: 2, marginBottom: 2 }}>
|
<Box sx={{ marginTop: 2, marginBottom: 2 }}>
|
||||||
<Typography variant="h5">{selectedShipment.shipment_name}</Typography>
|
<Typography variant="h5">{selectedShipment.shipment_name}</Typography>
|
||||||
|
<Box sx={{ display: 'flex', flexWrap: 'wrap' }}>
|
||||||
|
{renderPgroupChips()}
|
||||||
|
</Box>
|
||||||
<Typography variant="body1" color="textSecondary">
|
<Typography variant="body1" color="textSecondary">
|
||||||
Main contact person: {contact ? `${contact.firstname} ${contact.lastname}` : 'N/A'}
|
Main contact person: {contact ? `${contact.firstname} ${contact.lastname}` : 'N/A'}
|
||||||
</Typography>
|
</Typography>
|
||||||
@ -293,46 +332,78 @@ const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
|
|||||||
border: localSelectedDewar?.id === dewar.id ? '2px solid #000' : undefined,
|
border: localSelectedDewar?.id === dewar.id ? '2px solid #000' : undefined,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', marginRight: 2 }}>
|
<Box
|
||||||
{dewar.unique_id ? (
|
sx={{
|
||||||
<QRCode value={dewar.unique_id} size={70} />
|
display: 'flex', // Flex container to align all items horizontally
|
||||||
) : (
|
alignItems: 'center', // Vertically align items in the center
|
||||||
<Box
|
justifyContent: 'space-between', // Distribute children evenly across the row
|
||||||
sx={{
|
width: '100%', // Ensure the container spans full width
|
||||||
width: 70,
|
gap: 2, // Add consistent spacing between sections
|
||||||
height: 70,
|
}}
|
||||||
display: 'flex',
|
>
|
||||||
alignItems: 'center',
|
{/* Left: QR Code */}
|
||||||
justifyContent: 'center',
|
<Box>
|
||||||
border: '1px dashed #ccc',
|
{dewar.unique_id ? (
|
||||||
borderRadius: 1,
|
<QRCode value={dewar.unique_id} size={60} />
|
||||||
color: 'text.secondary'
|
) : (
|
||||||
}}
|
<Box
|
||||||
>
|
sx={{
|
||||||
<Typography variant="body2">No QR Code</Typography>
|
width: 60,
|
||||||
|
height: 60,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
border: '1px dashed #ccc',
|
||||||
|
borderRadius: 1,
|
||||||
|
color: 'text.secondary',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="body2">No QR Code</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Middle-Left: Dewar Information */}
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||||
|
<Typography variant="h6" fontWeight="bold">
|
||||||
|
{dewar.dewar_name}
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
<PuckDetailsVisual puckCount={dewar.number_of_pucks || 0} />
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||||
|
<CrystalFacetedIcon size={20} />
|
||||||
|
<Typography variant="body2">{dewar.number_of_samples || 0} Samples</Typography>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
</Box>
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box sx={{ flexGrow: 1 }}>
|
{/* Middle-Right: Contact and Return Information */}
|
||||||
<Typography variant="body1">{dewar.dewar_name}</Typography>
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, alignItems: 'flex-start' }}>
|
||||||
<Typography variant="body2">Number of Pucks: {dewar.number_of_pucks || 0}</Typography>
|
<Typography variant="body2" color="text.secondary">
|
||||||
<Typography variant="body2">Number of Samples: {dewar.number_of_samples || 0}</Typography>
|
Contact Person: {dewar.contact?.firstname ? `${dewar.contact.firstname} ${dewar.contact.lastname}` : 'N/A'}
|
||||||
<Typography variant="body2">Tracking Number: {dewar.tracking_number}</Typography>
|
</Typography>
|
||||||
<Typography variant="body2">
|
<Typography variant="body2" color="text.secondary">
|
||||||
Contact Person: {dewar.contact?.firstname ? `${dewar.contact.firstname} ${dewar.contact.lastname}` : 'N/A'}
|
Return Address: {dewar.return_address?.house_number
|
||||||
</Typography>
|
? `${dewar.return_address.street}, ${dewar.return_address.city}`
|
||||||
</Box>
|
: 'N/A'}
|
||||||
<Box sx={{
|
</Typography>
|
||||||
flexGrow: 1,
|
</Box>
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'space-between'
|
|
||||||
}}>
|
|
||||||
<CustomStepper dewar={dewar} selectedDewarId={localSelectedDewar?.id ?? null} refreshShipments={refreshShipments} />
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
|
{/* Right: Stepper */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
flexGrow: 1, // Allow the stepper to expand and use space effectively
|
||||||
|
maxWidth: '400px', // Optional: Limit how wide the stepper can grow
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CustomStepper
|
||||||
|
dewar={dewar}
|
||||||
|
selectedDewarId={localSelectedDewar?.id ?? null}
|
||||||
|
refreshShipments={refreshShipments}
|
||||||
|
sx={{ width: '100%' }} // Make the stepper fill its container
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
{localSelectedDewar?.id === dewar.id && (
|
{localSelectedDewar?.id === dewar.id && (
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user