diff --git a/backend/app/data/data.py b/backend/app/data/data.py index 7fe865e..d141b75 100644 --- a/backend/app/data/data.py +++ b/backend/app/data/data.py @@ -189,6 +189,7 @@ def generate_unique_id(length=16): dewars = [ Dewar( id=1, + pgroups="p20001, p20002", dewar_name="Dewar One", dewar_type_id=1, dewar_serial_number_id=2, @@ -204,6 +205,7 @@ dewars = [ ), Dewar( id=2, + pgroups="p20001, p20002", dewar_name="Dewar Two", dewar_type_id=3, dewar_serial_number_id=1, @@ -219,6 +221,7 @@ dewars = [ ), Dewar( id=3, + pgroups="p20004", dewar_name="Dewar Three", dewar_type_id=2, dewar_serial_number_id=3, @@ -234,6 +237,7 @@ dewars = [ ), Dewar( id=4, + pgroups="p20004", dewar_name="Dewar Four", dewar_type_id=2, dewar_serial_number_id=4, @@ -249,6 +253,7 @@ dewars = [ ), Dewar( id=5, + pgroups="p20001, p20002", dewar_name="Dewar Five", dewar_type_id=1, dewar_serial_number_id=1, diff --git a/backend/app/models.py b/backend/app/models.py index e55ed59..4cc988d 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -80,13 +80,14 @@ class Dewar(Base): __tablename__ = "dewars" 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_serial_number_id = Column( Integer, ForeignKey("dewar_serial_numbers.id"), nullable=True ) - tracking_number = Column(String(255)) - status = Column(String(255)) + tracking_number = Column(String(255), nullable=True) + status = Column(String(255), nullable=True) ready_date = Column(Date, nullable=True) shipping_date = Column(Date, nullable=True) arrival_date = Column(Date, nullable=True) diff --git a/backend/app/schemas.py b/backend/app/schemas.py index 4501e17..d07248d 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -523,6 +523,7 @@ class DewarCreate(DewarBase): class Dewar(DewarBase): id: int + pgroups: str shipment_id: Optional[int] contact: Optional[Contact] return_address: Optional[Address] diff --git a/frontend/src/assets/icons/CrystalIcon.tsx b/frontend/src/assets/icons/CrystalIcon.tsx new file mode 100644 index 0000000..63e9b13 --- /dev/null +++ b/frontend/src/assets/icons/CrystalIcon.tsx @@ -0,0 +1,18 @@ +import React from "react"; + +const CrystalFacetedIcon: React.FC<{ size?: number; color?: string }> = ({ size = 50, color = "#28A745" }) => ( + + {/* Facets */} + + + + +); + +export default CrystalFacetedIcon; \ No newline at end of file diff --git a/frontend/src/assets/icons/SimplePuckIcon.tsx b/frontend/src/assets/icons/SimplePuckIcon.tsx new file mode 100644 index 0000000..7ecbecf --- /dev/null +++ b/frontend/src/assets/icons/SimplePuckIcon.tsx @@ -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 = () => ( + + + {[...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 ; + })} + {[...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 ; + })} + +); + +// A wrapper component for displaying the puck icon and the count +export const PuckDetailsVisual: React.FC = ({ puckCount }) => { + return ( + + + + {puckCount} + + + ); +}; + +export default SimplePuckIcon; // Only SimplePuckIcon will be the default export \ No newline at end of file diff --git a/frontend/src/components/DewarStepper.css b/frontend/src/components/DewarStepper.css new file mode 100644 index 0000000..e150acd --- /dev/null +++ b/frontend/src/components/DewarStepper.css @@ -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; +} \ No newline at end of file diff --git a/frontend/src/components/DewarStepper.tsx b/frontend/src/components/DewarStepper.tsx index e717257..9f5ad2e 100644 --- a/frontend/src/components/DewarStepper.tsx +++ b/frontend/src/components/DewarStepper.tsx @@ -1,10 +1,13 @@ 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 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 }; @@ -16,15 +19,33 @@ const BottleIcon: React.FC<{ fill: string }> = ({ fill }) => ( ); // Icons Mapping -const ICONS: { [key: number]: React.ReactElement } = { - 0: , - 1: , - 2: , - 3: , - 4: , - 5: , +const ICONS: { + [key: number]: (props?: React.ComponentProps) => React.ReactElement; +} = { + 0: (props) => , + 1: (props) => ( + + ), + 2: (props) => , + 3: (props) => , + 4: (props) => ( + + ), + 5: (props) => ( + + ), }; - // StepIconContainer Component interface StepIconContainerProps { completed?: boolean; @@ -33,39 +54,42 @@ interface StepIconContainerProps { children?: React.ReactNode; } -const StepIconContainer: React.FC = ({ completed, active, error, children }) => { +const StepIconContainer: React.FC = ({ + completed, + active, + error, + children, + }) => { const className = [ completed ? 'completed' : '', active ? 'active' : '', error ? 'error' : '', - ].join(' ').trim(); + ] + .filter(Boolean) + .join(' '); return ( -
- {children} -
+
{children}
); }; -// StepIconComponent Props -type StepIconComponentProps = { - icon: number; - dewar: Dewar; - isSelected: boolean; - refreshShipments: () => void; -} & Omit, 'icon'>; - -// StepIconComponent -const StepIconComponent: React.FC = ({ icon, dewar, isSelected, refreshShipments, ...rest }) => { +const StepIconComponent: React.FC = ({ + icon, + dewar, + isSelected, + refreshShipments, + ...rest + }) => { const [anchorEl, setAnchorEl] = useState(null); - const handleIconEnter = (event: React.MouseEvent) => { - if (isSelected && icon === 0) { + const handleMenuOpen = (event: React.MouseEvent) => { + // Trigger menu ONLY for the BottleIcon (icon === 0) + if (icon === 0) { setAnchorEl(event.currentTarget); } }; - const handleIconLeave = () => { + const handleMenuClose = () => { setAnchorEl(null); }; @@ -98,13 +122,12 @@ const StepIconComponent: React.FC = ({ icon, dewar, isSe }; const { iconIndex, color } = getIconProperties(icon, dewar); - const IconComponent = ICONS[iconIndex]; return (
= ({ icon, dewar, isSe active={Boolean(rest['aria-activedescendant'])} error={rest.role === 'error'} > - {IconComponent - ? React.cloneElement(IconComponent, iconIndex === 0 ? { fill: color } : {}) - : Invalid icon - } + + {ICONS[iconIndex]?.({ + style: iconIndex === 0 ? { fill: color } : undefined, + }) ?? Invalid icon} + - setAnchorEl(anchorEl), - onMouseLeave: handleIconLeave, - }} - > - {['In Preparation', 'Ready for Shipping'].map((status) => ( - handleStatusChange(status as DewarStatus)}> - {status} - - ))} - + {icon === 0 && ( + setAnchorEl(anchorEl), // Keep menu open on hover + onMouseLeave: handleMenuClose, // Close menu when leaving + }} + > + {['In Preparation', 'Ready for Shipping'].map((status) => ( + handleStatusChange(status as DewarStatus)}> + {status} + + ))} + + )}
); }; - // Icon properties retrieval based on the status and icon number const getIconProperties = (icon: number, dewar: Dewar) => { const status = dewar.status as DewarStatus; @@ -166,16 +194,41 @@ const CustomStepper: React.FC = ({ dewar, selectedDewarId, r {steps.map((label, index) => ( } + StepIconComponent={(stepProps) => ( + + )} > - {label} + + {/* Step label */} + + {label} + + + {/* Optional: Date below the step */} + + {index === 0 + ? dewar.ready_date + : index === 1 + ? dewar.shipping_date + : index === 2 + ? dewar.arrival_date + : index === 3 + ? dewar.returning_date + : ''} + - - {index === 0 ? dewar.ready_date : - index === 1 ? dewar.shipping_date : - index === 2 ? dewar.arrival_date : - index === 3 ? dewar.returning_date : ''} - ))} diff --git a/frontend/src/components/ShipmentDetails.tsx b/frontend/src/components/ShipmentDetails.tsx index 77ccad1..0523184 100644 --- a/frontend/src/components/ShipmentDetails.tsx +++ b/frontend/src/components/ShipmentDetails.tsx @@ -1,5 +1,5 @@ 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 DeleteIcon from "@mui/icons-material/Delete"; 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 CustomStepper from "./DewarStepper"; import DewarDetails from './DewarDetails'; +import { PuckDetailsVisual } from '../assets/icons/SimplePuckIcon'; +import CrystalFacetedIcon from "../assets/icons/CrystalIcon.tsx"; + const MAX_COMMENTS_LENGTH = 200; @@ -183,6 +186,39 @@ const ShipmentDetails: React.FC = ({ const isCommentsEdited = comments !== initialComments; 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 No associated pgroups; + } + + return pgroupsArray.map((pgroup: string) => ( + + )); + }; + return ( {!localSelectedDewar && !isAddingDewar && ( @@ -229,6 +265,9 @@ const ShipmentDetails: React.FC = ({ {selectedShipment.shipment_name} + + {renderPgroupChips()} + Main contact person: {contact ? `${contact.firstname} ${contact.lastname}` : 'N/A'} @@ -293,46 +332,78 @@ const ShipmentDetails: React.FC = ({ border: localSelectedDewar?.id === dewar.id ? '2px solid #000' : undefined, }} > - - {dewar.unique_id ? ( - - ) : ( - - No QR Code + + {/* Left: QR Code */} + + {dewar.unique_id ? ( + + ) : ( + + No QR Code + + )} + + + {/* Middle-Left: Dewar Information */} + + + {dewar.dewar_name} + + + + + + {dewar.number_of_samples || 0} Samples + - )} - + - - {dewar.dewar_name} - Number of Pucks: {dewar.number_of_pucks || 0} - Number of Samples: {dewar.number_of_samples || 0} - Tracking Number: {dewar.tracking_number} - - Contact Person: {dewar.contact?.firstname ? `${dewar.contact.firstname} ${dewar.contact.lastname}` : 'N/A'} - - - - - + {/* Middle-Right: Contact and Return Information */} + + + Contact Person: {dewar.contact?.firstname ? `${dewar.contact.firstname} ${dewar.contact.lastname}` : 'N/A'} + + + Return Address: {dewar.return_address?.house_number + ? `${dewar.return_address.street}, ${dewar.return_address.city}` + : 'N/A'} + + + {/* Right: Stepper */} + + + + {localSelectedDewar?.id === dewar.id && ( {